JSON Web Tokens (or JWT) is a tried and true technique used to authenticate users & systems against stateless systems (mainly API).
But what happens when, on top of your authentication flow, you also want to include or integrate authorization?
That's right, we're now opening up that can of worms, and lucky for you, there is a straightforward answer: JWT + Cerbos.
Let's take a look at how we can do this without breaking a sweat.
If for some reason you're reading this and you don't know what the blip is Cerbos, I got your back: Cerbos is an open-source, policy-based authorization engine designed to make authorization simple, scalable, and maintainable.
That's a fancy way of saying that with Cerbos you can decouple your authorization logic from your application's code, allowing you to define, manage, and enforce complex access control policies easily.
Check out Cerbos' Github project if you'd like to know more about their OSS solutions (called PDP), or check out Cerbos Hub if you don't want to worry about anything.
Just to make sure we're all on the same page, while JWT is great for authentication (A.K.A AuthN), it's not that great for authorization (A.K.A AuthZ). Which is where Cerbos comes in. Comment Suggest edit
However, what's the difference between AuthZ and AuthN?
Authentication is the process of verifying the identity of a user or system. In digital systems, authentication typically involves methods like passwords, biometric scans, multi-factor authentication, or, you guessed it, token exchange (as in JSON Web Tokens). The goal is to establish the authenticity of the user before granting them access to the system and in following requests performed by the user.
Authorization, on the other hand, comes into play after authentication. It’s the process of determining what an authenticated user is allowed to do once inside the system.
In short: With AuthN you get through the door. With AuthZ you know what you can interact with.
Granted, JWT can be implemented in any language, and given how popular the methodology is, no matter what programming language you're using, chances are there is a library/module for you to use.
However, since we're showing you a practical example, here's how you can implement the JWT + Cerbos combo in JavaScript (specifically using NextJS).
To give you a simple example of how to mix these 2 technologies, we're going to implement a simple set of API routes:
The logic from step #2 will be mainly handled by Cerbos (that way, we don't have to worry about it).
First, let's set up a Next.js project for us to work on. You can easily do that, assuming of course you've already installed Node.js on your dev computer:
npx create-next-app jwt-next-cerbos
cd jwt-next-cerbos
npm install jsonwebtoken @cerbos/grpc
With those 3 lines, we've:
Let's now set up the API route for logging into our simple service.
Create these two files: app/login/route.js
and app/secured/route.js
:
app/login/route.js
import jwt from 'jsonwebtoken';
import { NextResponse } from 'next/server';
const SECRET_KEY = 'your_secret_key';
export const POST = async (req) => {
const { username, password } = await req.json();
// Replace with your user validation logic
if (username === 'admin' && password === 'password') {
const token = jwt.sign({ username, role: 'admin' }, SECRET_KEY);
return NextResponse.json({ token });
} else if (username === 'user' && password === 'password') {
const token = jwt.sign({ username, role: 'user' }, SECRET_KEY);
return NextResponse.json({ token });
} else {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 403 });
}
};
This file takes the username and password received as part of the request, and checks if they're correct.
Here we would usually validate the data against a secured database, but considering the simplicity of this example, we're hardcoding everything!
If the username/password combo is correct, we're creating the token, and for that, we need to "sign" the token that will be generated using the data received in the first argument of the jwt.sign
method.
Note that here we're passing a JSON object with the username and their role ('admin'). This information will be useful in a second, so remember this.
The second attribute is the secret key used, this is, obviously, a secret value that only you should know. This value will also be used as part of the validation logic to make sure the token received on the secured endpoint is valid (it won't be valid if it was signed using a different secret value).
Let's now set up the secured endpoint.
This is essentially a regular endpoint, with the addition of using Cerbos to check if we're actually allowed to access the resource.
app/secured/route.js
import jwt from 'jsonwebtoken';
import { GRPC } from '@cerbos/grpc';
import { NextResponse } from 'next/server';
const SECRET_KEY = 'your_secret_key';
const cerbos = new GRPC('localhost:3593', {tls: false});
export const GET = async (req) => {
const token = req.headers.get('authorization');
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
try {
const decoded = jwt.verify(token, SECRET_KEY);
const { role } = decoded;
const resource = {
resourceKind: 'document',
resourceId: '123',
policyVersion: 'default',
actions: ['read'],
};
const decision = await cerbos.check(role, resource);
if (decision.isAllowed('read')) {
return NextResponse.json({ message: 'This is a protected route and you have access to it!', user: decoded });
} else {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
} catch (err) {
return NextResponse.json({ error: 'Invalid token' }, { status: 403 });
}
};
This endpoint is doing the following:
If we are, indeed, allowed to read the endpoint, we'll get a successful response. Otherwise, we'll get a 403 error (read more on the 403 and 401 error codes here).
By now you have your endpoints defined, however, we've not executed Cerbos anywhere, nor have we properly defined the access rules. So let's do just that.
Create a simple policies file inside the /policies
folder. Keep in mind this is going to be a YAML file.
Let's create the policies/document.yaml
file that looks like this:
policies/document.yaml
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: document
rules:
- actions:
- create
- read
- update
- delete
effect: EFFECT_ALLOW
roles:
- admin
- actions:
- read
effect: EFFECT_ALLOW
roles:
- user
This policy file specifies that for all resources of type "document", admins can do everything and the rest (users or less) have only read access.
We now have to execute Cerbos somehow. There are several options, but the easiest one is to use a Docker image and run it using the following line:
docker run --rm --name cerbos -t \
-v $(pwd)/policies:/policies \
-p 3592:3592 \
ghcr.io/cerbos/cerbos:latest server
The final step is to run the server and test our logic.
To start your server, use the simple command:
npm run dev
1. Obtain JWT token by logging in
To test the login route and obtain a JWT token, run the following curl command:
curl -X POST http://localhost:3000/app/login -H "Content-Type: application/json" -d '{"username": "admin", "password": "password"}'
If the credentials are valid, you should receive a response containing a JWT token, like:
{
"token": "your_jwt_token_here"
}
2. Access the secured route
After obtaining the token, you can use it to access the /secured
route:
curl -X GET http://localhost:3000/app/secured -H "Authorization: your_jwt_token_here"
Replace your_jwt_token_here
with the token you received in the previous step.
If the authorization is successful, you should receive a response similar to:
{
"message": "This is a protected route",
"user": {
"username": "admin",
"role": "admin"
}
}
In the end, using JWT for authentication is quite straightforward, as long as you remember to keep your secret key… secret, of course.
And in terms of authorization, it would be quite the enterprise to build it, however, thanks to Cerbos, we're able to have everything up & running in minutes, and all the code we need to add is just an IF statement.
Are you using JWT already on your system? Check out Cerbos' open-source version - Cerbos PDP, give it a try, and then realize you've been wasting a lot of your valuable time implementing something that's already been solved.
Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team
Join thousands of developers | Features and updates | 1x per month | No spam, just goodies.