Using Cerbos with Supabase

Published by Fernando Doglio on November 19, 2024
Using Cerbos with Supabase

Supabase is an open-source Firebase alternative known for providing a backend with a real-time database, authentication, and APIs. In other words, it's a powerhouse giving you (almost) everything you need for your backend.

However, while Supabase offers powerful authentication mechanisms, if your system is complex enough, you'll reach a point where you'll need to complement this with robust authorization controls to manage what authenticated users can (or can't) access or modify.

And before you start planning the development of such system (I know what you're thinking!), take a moment to breathe and consider that Cerbos is the perfect complement to Supabase, bringing those advanced authorization capabilities that are missing.

Let's explore how you can seamlessly combine these two without breaking a sweat.

What exactly is Cerbos?

In case you accidentally stumbled into this article and are wondering what Cerbos is, here's a quick rundown.

Cerbos is an open-source, policy-based authorization engine that decouples your authorization logic from your application's code. This makes it easy to define, manage, and enforce complex access control policies at scale.

In simple terms, Cerbos simplifies how you manage who gets to do what within your application.

Want more details? Check out Cerbos’ GitHub project or explore Cerbos Hub to get a managed solution.

Integrating Supabase authentication with Cerbos

To demonstrate how to combine Supabase’s authentication and Cerbos’ authorization, we’ll create a simple set of API routes in a Node.js project.

1. Set up your project

Start by initializing a Node.js project and installing the required dependencies:

mkdir supabase-cerbos-auth
cd supabase-cerbos-auth
cd supabase-cerbos-auth
npm init -y
npm install @supabase/supabase-js @cerbos/grpc jsonwebtoken express

With those commands, you’ve set up a new project and installed the Supabase client, Cerbos gRPC client, JWT for handling tokens, and Express for setting up our API routes.

2. Configure Supabase

First, create a file named config.js to initialize the Supabase client:

// config.js
import { createClient } from '@supabase/supabase-js';

const SUPABASE_URL = 'https://your-project.supabase.co';
const SUPABASE_KEY = 'your_anon_key';

export const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

Make sure to replace the SUPABASE_URL and SUPABASE_KEY with your actual project’s details from the Supabase dashboard.

3. Create the users programmatically

For this, you'll want to go to your Project's configuration page, and disable the confirmation email & secure email change functionalities.

This will let us create our users programmatically with the following code:

seeds.js

import { supabase } from './config.js';

const { data, error } = await supabase.auth.signUp({
  email: 'myadmin@email.com',
  password: 'admin-password',
  options: {
    data: {
      user_role: "admin",
    },
  },
})

if (error) {
  console.log(error)
}


const { data_user, error_user } = await supabase.auth.signUp({
  email: 'myuser@email.com',
  password: 'user-password',
  options: {
    data: {
      user_role: "user",
    },
  },
})

if (error_user) {
  console.log(error_user)
}

Running this script will create the two users we need for our tests.

4. Create express routes with Cerbos

Now, let's define two routes in our Express app:

  1. A login route that authenticates a user through Supabase and returns a JWT token.
  2. A protected route that validates the session and uses Cerbos to authorize access.

Here’s the code:

server.js

import express from 'express';
import { supabase } from './config.js';
import { GRPC } from '@cerbos/grpc';
import jwt from 'jsonwebtoken';

const app = express();
const PORT = 3000;
const cerbos = new GRPC('localhost:3592', { tls: false });

app.use(express.json());

// Login route
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const { data, error } = await supabase.auth.signInWithPassword({ email, password });
  console.log("Sign in data: ", data)

  if (error) {
    return res.status(403).json({ error: 'Invalid credentials' });
  }

  // Create a JWT token with the user data
  const token = jwt.sign({ userId: data.user.id  }, 'secret_key');
  res.json({ user: data.user, token: data.session.access_token });
});

// Secured route
app.get('/secured', async (req, res) => {
  // Get the token from the Authorization header
  const token = req.headers?.authorization?.split(' ')[1];

  try {
    // Use Supabase to get the user data
    const { data: { user }, error } = await supabase.auth.getUser(token);
    if (error || !user) {
      console.log(error);
      return res.status(401).json({ error: 'Unauthorized' });
    }

    //We're definig access to a resource of type "document" with an ID of "123"
    const payload = { 
      resource: {
        kind: 'document',
        id: '123',
        policyVersion: 'default',
      },
      principal: {
        id: user.id,
        roles: [user.user_metadata.user_role],
      },
      action: 'read'
    }

    const decision = await cerbos.isAllowed(payload);
    console.log(decision)
    if (decision) {
      return res.json({ message: 'You have access to this secured route!' });
    } else {
      return res.status(403).json({ error: 'Forbidden' });
    }
  } catch (error) {
    console.error(error);
    return res.status(401).json({ error: 'Unauthorized' });
  }
});

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

5. Define Cerbos policies

Let's now create a policy that will only let admins do everything with documents, and only allow users to read them. Make sure to put the policy file inside a policies folder:

policies/document.yaml

# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
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

6. Start Cerbos

We now have to run Cerbos locally, for that we can use the following Docker command:

docker run --rm --name cerbos -t \
  -v $(pwd)/policies:/policies \
  -p 3592:3592 \
  ghcr.io/cerbos/cerbos:latest server

Or if you're using PowerShell on Windows:

docker run --rm --name cerbos -t -v "${PWD}/policies:/policies" -p 3592:3592 ghcr.io/cerbos/cerbos:latest server

7. Test the integration

First off, let's start your server with:

node server.js

Log in to create a session

curl -X POST http://localhost:3000/login -H "Content-Type: application/json" -d '{"email": "myadmin@email.com", "password": "admin-password"}'

This command should return a JSON in the response body. That JSON should have a property called "token", keep the value of that field, because we'll be using it in a second.

Access the secured route

curl -X GET http://localhost:3000/secured -H "Authorization: Bearer <your token>"

Supabase will keep the session open on its side, so you don't need to send anything extra.

Final thoughts

With Supabase handling your authentication needs and Cerbos streamlining authorization, you’ve got yourself a scalable and secure setup in just a few minutes.

Supabase’s integration with Cerbos means you no longer need to worry about writing complex access control logic.

Using Cerbos with Supabase isn’t just a time-saver—it’s a way to build secure applications with maintainable policies.

Are you looking to level up your AuthZ game? Give Cerbos’ open-source solution - Cerbos PDP - a try.

Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team