restful-json-api-client
This package provides you an easy way of creating RESTful API JavaScript classes.
Features
- Classes extending
RestfulClient
are enriched of the CRUD actions (all
,get
,create
,update
anddestroy
) (See more) - Custom actions and headers (See more)
- Custom requests (See more)
- Automatic JWT (See more)
Installation
yard add restful-json-api-client
Usage
configure
This library provide a configure
function which allow you to set some options
used across all instances of the library.
Here is an example of how to use it and the available options with their default values:
RestfulClient.configure({
/*
** Given you need to have particular headers in each of the requests sent by
* this library, here you can set them.
*
* For example, given you have a Rails project, requiring the CSRF token,
* here is an example of how to fix that:
* ```javascript
* RestfulClient.configure({
* headers: {
* 'X-API-Version': document.querySelector('[name="api-version"]').content,
* 'X-CSRF-Token': () => document.querySelector('[name="csrf-token"]').content
* }
* })
* ```
*/
headers: null,
/*
** Default content of the `credentials` field of the requests sent by this
* library.
* When this attribute is `null` the request's credentials attribute is not
* assigned remaining to its default.
*/
requestCredentials: null,
/*
** Controls where this library stores the tokens (access and refresh tokens)
* using localStorage by default.
* When this attribute is set to `local`, both tokens are written in the
* `localStorage`. (The default)
* When this attribute is set to `session`, both tokens are written in the
* `sessionStorage`.
* When this attribute is an object, each token follow the configured storage.
* This allows you to store one in `localStorage`, the other one in the
* `sessionStorage`.
* Example:
* ```javascript
* RestfulClient.configure({
* storage: {
* accessToken: 'session', // The `access_token` will be stored in the `sessionStorage`
* refreshToken: 'local', // The `refresh_token` will be stored in the `localStorage`
* }
* })
* ```
*/
storage: 'local',
/*
** Response's body attribute name where the authentication token should be
* retrieved, stored and included in all future requests.
* See the "Token/JWT" section bellow.
*/
tokenAttributeName: 'token',
/*
** Response's body attribute name containing the above defined attribute where
* the authentication token could be retrieved.
* When this attribute is `null` the token attribute is expected to be found
* from the body's root.
*/
tokenParent: null,
/*
** URL path where this library should send a POST request when receiving a 401
* response from any request.
* When this attribute is `null`, and a 401 response code is found, no
* renewing tries will be performed and the 401 is directly forwarded to your
* application.
* See the "Token/JWT" section bellow.
*/
tokenRenewPath: null,
/*
** Given you configured the above `tokenRenewPath` option, you can pass a
* callback function to be called when renewing a token succeeded.
* See the "Token/JWT" section bellow.
*/
tokenRenewCallback: null,
/*
** When this library is used with OAuth, configured to provide a
* `refresh_token` when successfully authenticating, this attribute should be
* set to `true` so that the refresh token is retrieved, stored and used to
* renew an access token when it expires instead of using the access token
* itself.
* See the "Token/JWT" section bellow.
*/
withRefreshToken: false
})
Most simple use case
This example creates a UsersApi
class, which allows you to request any of the
CRUD actions :
import RestfulClient from 'restful-json-api-client'
export default class UsersApi extends RestfulClient {
constructor () {
super(
'https://myapp.com/api', { // The base URL of the API to consume
resource: 'users' // The resource of the API to consume
})
}
}
Note: When passing a /
as baseUrl (First argument),
the window.location.origin
is used.
From now on, you can instantiate it and call CRUD actions :
const usersApi = new UsersApi()
usersApi.all() // Calls a GET on https://myapp.com/api/users
usersApi.get({ id: 1 }) // Calls a GET on https://myapp.com/api/users/1
usersApi.create({ name: 'zedtux' }) // Send a POST to https://myapp.com/api/users
usersApi.update(2, { name: 'john' }) // Send a PATCH to https://myapp.com/api/users/2
usersApi.destroy(2) // Send a DELETE to https://myapp.com/api/users/2
Non-CRUD actions
You can also request non CRUD actions :
const usersApi = new UsersApi()
usersApi.request('POST', path: 'auth', body: {
username: 'johndoe',
password: 'p4$$w0rd'
}).then(response => response.token) // Successfully logged in
.then(token => saveToken(token)) // Remember your credentials
.catch(err => alert(err.message)) // Catch any error
Complex use case
In this example we :
- Set a custom header field
- Add custom actions
import RestfulClient from 'restful-json-api-client'
export default class PositionsApi extends RestfulClient {
constructor (authToken) {
super('https://api.myapp.com', {
resource: 'positions',
headers: {
'X-Custom-Field': 'true',
'X-Custom-Field-2': 'zedtux'
}
})
}
getWeather (date) {
// The body object will be used to build a query.
// For example, in the case `date` is `{ "lt": "2018/07/13" }` the GET query
// will be https://api.myapp.com/positions/weather?lt="2018/07/13"
return this.request('GET', { path: 'weather', body: { date } })
.then(response => response.data)
}
checkIn (lat, lon) {
// In this other example, the body object will be used as the request body.
// A request to https://api.myapp.com/positions/checkIn will be sent.
return this.request('POST', { path: 'checkin', body: { lat, lon } })
}
}
Empty headers
In the case you need this library to not set any headers when sending requests,
you can pass headers: false
and the headers will be empty:
export default class MinionsApi extends RestfulClient {
constructor (authToken) {
super('https://api.myapp.com', {
resource: 'minions',
headers: false
})
}
}
credentials
field for one resource
Setting the request Given a resource API requires the fetch request's credentials
property to be
set, you can pass it as an option.
In the case you've configured the requestCredentials
, passing a different
credentials
here will override it.
export default class MinionsApi extends RestfulClient {
constructor (authToken) {
super('https://api.myapp.com', {
credentials: 'same-origin',
resource: 'minions',
})
}
}
Token/JWT
This library detects tokens from API response body (looking for a token
attribute) when there's one, like when creating sessions using JWT
authentication mechanism.
When a token has been detected, it is stored in the localStorage
within the
key restfulclient:jwt
, and injected in future queries headers.
At any time you can call the RestfulClient.reset()
function in order to clear
the token, so that next queries will no more include the Authorization
header.
About storing the token in the browser
As of writing, there's no secure and reliable place where to store secrets in the browser.
An attacker can steal secrets from local and session storage, and memory. Service worker support is poor (not all web browser support it, and it doesn't support websockets).
You can't fight stealling secrets, so you only can prevent their usage using a fingerprint (raw fingerprint in a HttpOnly + Secure + SameSite + Max-Age + cookie prefixes cookie, the SHA256 of the raw fingerprint in the token, and comparing both in order to allow using the token).
Storing in a secure place is then not the role of this library, that's why it
stores the token in the localstorage
.
Nonetheless to reduce the attack surface, you can store the access_token
in
the sessionStorage
, so that new tabs/windows doesn't have it, and store the
refresh_token
in the localStorage
making it accessible from anywhere and
allow a new tab to request a new token.
Thanks to the fingerprint cookie that the attacker wont have, he wouldn't be
able to request new access_token
.
Here is how you should configure this library in order to achieve that:
RestfulClient.configure({
storage: {
accessToken: 'session',
refreshToken: 'local'
},
tokenAttributeName: 'access_token',
tokenRenewPath: '/path/to/the/renew',
withRefreshToken: true
})
Auto renewal
A token should expire, and when that happen, you're not force to logout your users, you can renew it.
A first way could be to include the new token in the next response from your
backend, and as restful-json-api-client
is constantly looking for a token,
it will refresh the token and use the fresh one in future requests.
The second and recommended way is to configure a renewal path:
RestfulClient.configure({
tokenRenewPath: '/api/sessions'
})
When hitting a 401 error, restful-json-api-client
will automatically try a
POST
request to the configured path including the expired token, and if your
backend replies with a fresh token, restful-json-api-client
will update the
stored token allowing to re-run the failed request but with the new token, and
future requests will use the new token.
Refresh token
This library supports refresh tokens, used to request a new access token when the latter has expired.
In order to enable this mode, you have to pass withRefreshToken: true
to the
RestfulClient.configure()
function and this will make this library:
- Looking for a
refresh_token
from the token creation request - Storing the
refresh_token
found in thelocalStorage
- Using the refresh token, passed in a
refresh_token
parameter when calling the configuredtokenRenewPath
API
TODO : When the token API gives an expires_in
property, use a setTimeout
to
renew the access token before it expires.
Renew callback
In the case you need a callback function in your app to be called on renewing the token with success, you can configure a callback function :
RestfulClient.configure({
tokenRenewPath: '/api/sessions',
tokenRenewCallback: (newJwt) => {
console.log('Renewed JWT', newJwt)
}
})
Token Custom property name
This library expects, by default, to find the token within a token
property
from the response body, but you can of course configure it:
RestfulClient.configure({
tokenAttributeName: 'access_token'
})
Now restful-json-api-client
look at a property named access_token
in order
to retrieve the token.
Also on renewing, the configured name is used to post the token. In the above
example, the POST
request to the renew API will have a body
like the
following:
{
access_token: '<here the expired token>'
}
When the JWT is embedded in an object
The token could be within an object from the response body. In this case you can
configure restful-json-api-client
in order to look within a named object.
For example, let's take the following response body:
{
"user": {
"created_at": "2020-10-12T08:19:12.023+00:00",
"email": "admin@test",
"id": "9dKUOThuEe6gCm",
"updated_at": "2020-10-12T08:19:12.023+00:00",
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOWRLVU9UaHVFZTZnQ20iLCJleHAiOjE2MDI1ODMzODh9.FmQlIQkexCdzT9NYd6ch-bCWzHwxoU4cQienc63k28g"
}
}
You can make this library looking for the token from the user
object with:
RestfulClient.configure({
tokenParent: 'user'
})
Running the tests without Docker
- Install the dependencies:
yarn
- Run the tests:
yarn test
Running the tests with Docker
- docker pull node:latest
- docker-compose run --rm test
Publish
npm login
- Update version in the
package.json
file - Update the
CHANGELOG.md
file c run --rm publish
License
MIT