Tutorial: Using Cerbos with JWT

An example application of integrating Cerbos with an Express server using JSON Web Tokens - via express-jwt - for authentication.

Dependencies

Getting started

  1. Clone the repo

    git clone git@github.com:cerbos/express-jwt-cerbos.git
  2. Start up the Cerbos PDP instance docker container. This will be called by the express app to check authorization.

    cd cerbos
    ./start.sh
  3. Install node dependencies

    npm install
  4. Start the express server

    npm run start

Policies

This example has a simple CRUD policy in place for a resource kind of contact - like a CRM system would have. The policy file can be found in the cerbos/policies folder here.

Should you wish to experiment with this policy, you can try it in the Cerbos Playground.

The policy expects one of two roles to be set on the principal - admin and user. These roles are authorized as follows:

Action User Admin

list

Y

Y

read

Y

Y

create

N

Y

update

N

Y

delete

N

Y

This business logic is represented in Cerbos as a resource policy.

apiVersion: api.cerbos.dev/v1
resourcePolicy:
  version: default
  resource: contact
  rules:
  - actions: ["read", "list"]
    roles:
      - admin
      - user
    effect: EFFECT_ALLOW

  - actions: ["create", "update", "delete"]
    roles:
      - admin
    effect: EFFECT_ALLOW

JWT Structure

For this example a JWT needs to be generated to be passed in the authorization header. The payload of the token contains an array of roles which are passed into Cerbos to use for authorization - the structure is as follows:

{
  sub: string,
  name: string,
  iat: number,
  roles: string[] // "user" and "admin" supported in this demo
}

JWT.io can be used generate a token for testing purposes - an example is here.

Note: The secret is hardcoded in this example to yoursecret and the algorithm is HS256 - you will need to set these for the signature to be valid.

JWT

Request Flow

  1. HTTP request comes in and the express-jwt library validates the token and adds the payload to req.user.

  2. The contents of the JWT token is mapped to the structure of the principal object required by Cerbos

// Extract data from the JWT (check DB etc) and create the principal object to be sent to Cerbos
const jwtToPrincipal = ({ sub, iat, roles = [], ...rest }) => {
  return {
    id: sub,
    roles,
    attr: rest,
  };
};
  1. Fetch the data required about the resource being accessed from the data store

  2. Call the Cerbos PDP with the principal, resource and action to check the authorization and then return an error if the user is not authorized. The Cerbos package is used for this.

const allowed = await cerbos.check({
  principal: jwtToPrincipal(req.user),
  resource: {
    kind: "contact",
    instances: {
      //a map of the resource(s) being accessed
      [contact.id]: {
        attr: contact,
      },
    },
  },
  actions: ["read"], //the list of actions being performed
});

// not authorized for read action
if (!allowed.isAuthorized(contact.id, "read")) {
  return res.status(403).json({ error: "Unauthorized" });
}
  1. Serve the response if authorized

Example Requests

Once a JWT token has been generated requests can be made to the express server.

List contacts

Allowed for user and admin roles

curl -X GET 'http://localhost:3000/contacts' \
--header 'Authorization: Bearer <token here>'

Get a contact

Allowed for user and admin roles

curl -X GET 'http://localhost:3000/contacts/abc123' \
--header 'Authorization: Bearer <token here>'

Create a contact

Allowed for admin role only

curl -X POST 'http://localhost:3000/contacts/new' \
--header 'Authorization: Bearer <token here>'

Should this request be made with the JWT roles set to ["admin"] the response will be"

{ "result": "Created contact" }

Should this request be made with the JWT roles set to ["user"] the response will be:

{ "error": "Unauthorized" }

Update a contact

Allowed for admin role only

curl -X PATCH 'http://localhost:3000/contacts/abc123' \
--header 'Authorization: Bearer <token here>'

Should this request be made with the JWT roles set to ["admin"] the response will be"

{ "result": "Contact updated" }

Should this request be made with the JWT roles set to ["user"] the response will be:

{ "error": "Unauthorized" }

Delete a contact

Allowed for admin role only

curl -X DELETE 'http://localhost:3000/contacts/abc123' \
--header 'Authorization: Bearer <token here>'

Should this request be made with the JWT roles set to ["admin"] the response will be"

{ "result": "Contact deleted" }

Should this request be made with the JWT roles set to ["user"] the response will be:

{ "error": "Unauthorized" }