restful-json-api-client

1.4.12 • Public • Published

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 and destroy) (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
    })
  }
}

Setting the request credentials field for one resource

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 the localStorage
  • Using the refresh token, passed in a refresh_token parameter when calling the configured tokenRenewPath 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

  1. Install the dependencies: yarn
  2. Run the tests: yarn test

Running the tests with Docker

  1. docker pull node:latest
  2. docker-compose run --rm test

Publish

  1. npm login
  2. Update version in the package.json file
  3. Update the CHANGELOG.md file
  4. c run --rm publish

License

MIT

Package Sidebar

Install

npm i restful-json-api-client

Weekly Downloads

3

Version

1.4.12

License

MIT

Unpacked Size

79 kB

Total Files

16

Last publish

Collaborators

  • zedtux