Accessing Protected Resources in Next.js with Prisma

Published by Sam Lock on February 24, 2023
Accessing Protected Resources in Next.js with Prisma

In this blog post, we'll be focusing on how to integrate Next.js and Prisma, two popular technologies for building modern web applications, with Cerbos, the open-source, decoupled authorization layer for your software.

By combining the power of Next.js for creating fast, server-rendered React applications with the flexible, data-driven capabilities of Prisma, you can create dynamic and secure web experiences for your users. By integrating an authorization layer, you can control access to your application's resources, providing an extra layer of security and protection for your data. Join us as we explore this integration and discover how you can elevate your Next.js applications to the next level.

You can check out the code for this demo here.

What we’re building

A simple web application which allows you to log in as one of a selection of predefined users (each with different roles and attributes), and then display the contacts that the given user has access to. Consisting of (in a nutshell):

  • Mocked out authentication via a static set of users defined in memory (instantiated on startup)
  • Session management with Passport.js
  • APIs for login/logout
  • Server-side rendered pages, with prefetching of allowed resources using Cerbos authorization combined with Prisma via our query plan adapter

The database layer

First things first, we need to configure our database. In this demo, we streamline the process significantly by relying on a SQLite instance which persists to local disk. With Prisma, this is very simple to configure. Check out this section of prisma/schema.prisma:

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

This tells Prisma that we want to use a SQLite database, and to persist it to a relative file at ./dev.db.

Check out the rest of the file, you’ll see some model definitions further down which Prisma will use when instantiating the database. Each model relates to a table definition, e.g.:

model User {
  id         String    @id @default(cuid())
  username   String    @unique
  email      String    @unique
  name       String?
  contacts   Contact[]
  department String
}

Once we’ve defined our DB provider and our data models, we tell Prisma to create out SQLite database and tables with the following:

npx prisma migrate dev --name init

You’ll see the prisma/migrations/ directory will have appeared, and the client will be ready to talk to the DB.

A note on authentication

For the purposes of this demo, we’ve mocked out the Identity Provider (IdP) by simply defining some static users in code, which will be instantiated and held in memory when the app is fired up. You can see this in the aptly named lib/idp.ts file:

export const users = [
  {
    username: "alice",
    password: "supersecret",
    role: "admin",
  },
  …
  },
];

In production, you could substitute this for your own IdP, whether that be a hosted solution or a custom one.

Prefetching protected content

Next.js provides a function called getServerSideProps, which allows you to fetch data on the server-side and pass it to the component as props. It is used to provide statically generated HTML pages whilst still allowing the data to be dynamic, providing a fast and efficient way to fetch and display data in a Next.js application.

In the context of authorization, this is hugely useful. We can leverage Cerbos and precompute access to resources before passing them to the client for rendering. Check out getServerSideProps in pages/contacts.tsx. A few things happen here: firstly, we check for an active session; if not present, we’ll redirect the user to the login page. Otherwise, we retrieve the user info from the session and build further contextual data via a call to the database with Prisma:

const user = await prisma.user.findUnique({
  where: { username: session.username },
});
if (!user) {
  throw Error("Not found");
}

We can then use this additional context to construct a payload to pass to the Cerbos query planner:

const contactQueryPlan = await cerbos.planResources({
  principal: {
    id: user.id,
    roles: [session.role],
    attributes: {
      department: user.department,
    },
  },
  resource: {
    kind: "contact",
  },
  action: "read",
});

And then use the handy query plan adapter to convert it into a Prisma query to retrieve all of the allowed resources for the principal:

let contacts: any[] = [];

const queryPlanResult = queryPlanToPrisma({
  queryPlan: contactQueryPlan,
  // map or function to change field names to match the prisma model
  fieldNameMapper: {
    "request.resource.attr.ownerId": "ownerId",
    "request.resource.attr.department": "department",
    "request.resource.attr.active": "active",
    "request.resource.attr.marketingOptIn": "marketingOptIn",
  },
});

Now we have our allowed resources (contacts), and the user data, we can pass these to the React component as props from the function:

return { props: { contacts, user } };

All of this computation occurs on the back end, so the client will only receive data for resources that we already know it has access to!

We use similar logic for the contact detail page, see pages/contacts/[cid].tsx. The difference here is that we construct contextual data for both the principal (the user) and the resource (the contact), and check access for that specific combination and the action "read":

const decision = await cerbos.checkResource({
  principal: {
    id: user.id,
    roles: [session.role],
    attributes: {
      department: user.department,
    },
  },
  resource: {
    kind: "contact",
    id: contact.id + "",
    attributes: JSON.parse(JSON.stringify(contact)),
  },
  actions: ["read"],
});

Deploying to Vercel

If you’ve pulled the code from the repo, running the app locally is as simple as running a couple of commands. But we can go a step further than that! Let’s go ahead and deploy our demo app to Vercel.

First things first, you'll need to create an account (if you haven't already) and connect your project to a git provider.

Once you've done that, we can make a couple of small adaptations to the code and use some handy tooling to enable us to host our streamlined demo on Vercel with no other hosted deployments required!

Database

The demo currently uses SQLite. Vercel offers ephemeral compute primitives without persistent storage (see here), so deploying the app as-is won't work.

In production, it's likely that you'd separately provision your database and then point your app at it, but for this demo, we can provision one locally and then use ngrok to expose that database to the public internet! At the time of writing, they offer a free tier allowing you to proxy a single service on a randomly generated URL at any one time.

The following steps will show you how to configure a local instance of PostgreSQL and adapt the code to talk to this DB instance via ngrok.

  1. Install and configure Postgres, and then start an instance
  2. Install and configure ngrok
  3. Expose your locally running Postgres instance with ngrok: ngrok tcp 5432 (change the port if necessary)
  4. Connect to the Postgres server (with psql) and create a Postgres user and database:
    CREATE USER test_user WITH PASSWORD 'password123' CREATEDB;
    CREATE DATABASE cerbos;
    
  5. Update datasource db in prisma/schema.prisma to point to Postgres via an environment variable:
    datasource db {
      provider = "postgres"
      url      = env("DATABASE_URL")
    }
    
  6. Instantiate the Prisma client with the new database (note: if you previously did this with SQLite, you'll need to delete the prisma/migrations/ folder prior to running the following):
    npx prisma migrate dev --name init
    npx prisma db seed
    
  7. Go to the "Environment Variables" section of the project settings in the Vercel dashboard, and set DATABASE_URL to the appropriate connection string in the format: postgresql://{user}:{password}@{ngrok_url}:{ngrok_port}/{db_name}. E.g. if the ngrok "forwarding" output was tcp://4.tcp.eu.ngrok.io:12348 -> localhost:5432, then (assuming the parameters in the example SQL above) the connection string would be postgresql://some_user:password1234@4.tcp.eu.ngrok.io:12348/cerbos.

Cerbos PDP

We also need access to an instance of the Cerbos PDP. Again, in production, you'd manage your Cerbos deployments separately, but for this demo we can rely on the Cerbos playground to store and serve our policies. For convenience, we've created a playground instance with the required policies which you can use here.

To point to this PDP instance, back in the Vercel project settings, set the environment variable CERBOS_URL to "demo-pdp.cerbos.cloud". You can optionally set CERBOS_PLAYGROUND_INSTANCE as well, but if you don't, it'll default to the provided instance.

Deploy

Deploy the preview to Vercel (running vercel in the project root will trigger this), and load it up. You should now have a Vercel hosted deployment, using an instance of Postgres on your local machine and a public instance of the Cerbos PDP running in the playground!

Productionizing

After the changes we made above, we now have an app deployed on Vercel, using a locally running database (through the magic of ngrok) and a convenient test instance of Cerbos. This is fine for prototyping, but how would we make the step to a full production system? There are (potentially) two major considerations for this:

Database

First things first, we need to provision a database. Needless to say, there are countless methods and hosting providers upon which we can do so. It’s even quite possible that you already have your database running in your production environment.

Adapting the code to point to your production database is as simple as updating the datasource db section in prisma/schema.prisma. Prisma supports a wide range of databases; update the provider, if necessary (you could handle this through an env var too) and set the url to the appropriate connection string! Prisma gives lots of help on this.

Cerbos deployment

The other missing component is a production ready instance of Cerbos. At the moment, we’re pointing to the Cerbos-provided playground instance. This is great for prototyping, but as touched on previously, not recommended for production use.

In the same vein as our database, the way you deploy Cerbos is entirely dependent on your own infrastructure and particular needs. Cerbos can be deployed in a variety of ways. Once it’s available, you can point your app to it by setting the CERBOS_URL environment variable.

Fin

By integrating Next.js, Prisma, and Cerbos, we have shown how to build a simple and secure web application with dynamic and protected content. The combination of Next.js' server-side rendering, Prisma's flexible data-driven capabilities, and Cerbos' authorization layer provides a powerful solution for building modern web experiences.


As always, if you have any questions or feedback, head over to our Slack community and ask away!

DOCUMENTATION
GUIDE
INTEGRATION

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