Skip to main content

Auth0 and Permit Integration

Add Permit.io authorization to an application that signs users in with Auth0. You create a policy in Permit, check permissions in your API routes, add Auth0 roles to the user's tokens with an Auth0 Action, and sync each user and their roles to Permit after login. This guide is for developers with an Auth0-based Next.js app. The examples come from the Auth0 demo application, so you can compare each step with working code.

For background on combining RBAC with Auth0, read the Permit blog post Add RBAC authorization to Auth0.

Prerequisites

  • An Auth0 account and an application that signs users in with Auth0. The examples use the @auth0/nextjs-auth0 SDK.
  • A Permit.io account and your environment API key. See Get your API key.
  • The Permit Node.js SDK installed in your app: npm install permitio.

1. Create the policy in Permit

Create the roles, resources, and permissions that your application checks. You can use the Permit dashboard, the Permit API, or one of the Permit SDKs.

  1. Create roles. If you manage roles in Auth0, create a Permit role for each Auth0 role, with the same key. Role keys are case-sensitive, so Admin in Auth0 doesn't match admin in Permit. If you don't use Auth0 roles, create the roles your application needs.
  2. Create resources. Create a Permit resource for each part of your application you protect, with the actions users take on it. For example, a task list needs a task resource with get, post, and delete actions. See the create resource API or the Policy Editor resources tab.
  3. Assign permissions. In the Policy Editor, check the actions each role can take on each resource, or use the assign permissions API.

2. Confirm the Auth0 session in your app

Permit decides what a user can do. Auth0 still decides who the user is. Your app needs both parts:

  • Frontend: redirect users who aren't signed in to the Auth0 login page. In the demo app, _app.tsx wraps the app in UserProvider, and index.tsx reads the user with useUser:
// index.tsx
//...
const { user, isLoading } = useUser();
//...
// _app.tsx
//...
<UserProvider>
<Component {...pageProps} />
</UserProvider>
//...
  • Backend: validate the Auth0 session and read the user from it. In the demo app, pages/api/auth/[...auth0].ts handles the Auth0 routes, and each API route calls getSession:
// pages/api/auth/[...auth0].ts
import { handleAuth } from "@auth0/nextjs-auth0";

export default handleAuth();
// pages/api/tasks.ts
import { getSession } from "@auth0/nextjs-auth0";
//...
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession(req, res); // if the user is not logged in, session will be null
if (!session?.user) {
res.status(401).json({ message: 'unauthorized' });
return;
}
//...

3. Check permissions in your API routes

Call permit.check() on the backend before each action you protect. You can call it in each API route or in middleware for the whole API. Pass the user's Auth0 ID (session.user.sub) as the user key, the action, and the resource:

const permitted = await permit.check(
session?.user.sub, // the user's id
"delete", // the action name
"task" // the resource name
);

permit.check() returns a promise. Await it before you use the result.

In the demo app, pages/api/tasks.ts creates the Permit client and checks every task request, using the HTTP method as the action. The client reads the API key from the PERMIT_SDK_TOKEN environment variable:

// pages/api/tasks.ts
import { Permit } from "permitio";

export const permit = new Permit({
pdp: "https://cloudpdp.api.permit.io",
token: process.env.PERMIT_SDK_TOKEN,
});
export default withApiAuthRequired(async function handler(
req: NextApiRequest,
res: NextApiResponse<Task | Task[] | Response>
) {
// Auth0 is checking if the user is logged in (who the user is)
const session = await getSession(req, res);
if (!session?.user) {
res.status(401).json({ message: "unauthorized" });
return;
}
// Permit is checking if the user has the right permissions (what the user can do)
const isAllowedForOperation = await permit.check(
(session?.user?.sub as string) || "", // the user identifier (Permit user id / or Permit user key, in this example we set the Auth0 user id as the key)
req.method?.toLowerCase() as string, // the action (can be: get, post, delete)
"task" // our resource key
);
if (!isAllowedForOperation) {
res.status(403).json({ message: "forbidden" });
return;
}
//... handle the request
});

The route returns HTTP 401 when there is no Auth0 session and HTTP 403 when Permit denies the action.

4. Add Auth0 roles to the user's tokens

Skip this step if you don't manage roles in Auth0.

Auth0 doesn't put a user's roles in the ID token or access token by default. Add them as a custom claim with an Auth0 Action on the Post Login trigger. For details, see the Auth0 guide Add user roles to tokens.

  1. In the Auth0 dashboard, go to Actions > Triggers.
  2. Under Sign Up & Login, click post-login.
  3. Click + to add an action, then select Build from scratch.
  4. Name the action, for example Add Roles to Tokens.
  5. Replace the default code with this Action code:
/**
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'my_app_name';
if (event.authorization) {
api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
}
};
  1. Click Deploy.
  2. Click the back arrow to return to the trigger. In the Post Login flow, drag your action into the flow.
  3. Click Apply.

After the user signs in again, session.user includes the roles under the my_app_name/roles claim. Replace my_app_name with a namespace for your app, and use the same claim name in step 5:

auth0User {
'my_app_name/roles': [ 'admin' ],
nickname: 'test',
name: 'test@permit.io',
picture: 'https://s.gravatar.com/avatar/test.png',
updated_at: '2023-07-05T10:56:35.831Z',
email: 'test@permit.io',
email_verified: false,
sub: 'auth0|64a139da377fefdcbxxxxxxx',
sid: 'I9xntALjR3M37Iw62Lgc3xxxxxxxxxx'
}

5. Sync the user and roles to Permit after login

Permit can only match a user to roles after the user exists in Permit. After login, sync the user with permit.api.syncUser(), then assign each Auth0 role with permit.api.assignRole(). Use the Auth0 user ID as the user key, so the key matches the one you pass to permit.check().

// sync the user with Permit
const userObj = {
key: auth0User.sub, // the user's key needs to be unique, you can use the Auth0 user id, we will use it to perform the permit checks.
email: auth0User.email,
first_name: auth0User.name,
attributes: {}, // you can add more attributes here, mostly used for ABAC
};

const permitUser = await permit.api.syncUser(userObj);

auth0User["my_app_name/roles"].map(async (roleKey) => {
// assign the roles to the user
await permit.api.assignRole({
user: userObj.key, // the user's key
role: roleKey, // the role key
tenant: "default", // the tenant key; use `default` if you don't use multi-tenancy
});
});

In the demo app, the Auth0 login handler returns the user to a /postLogin page, and that page's getServerSideProps runs the sync. Read Sync Auth0 with Permit in the demo application, or the full postLogin page on GitHub.

Keep role changes made in Permit

The sync in step 5 assigns every Auth0 role on each login. If you remove a role in Permit while the user still has it in Auth0, the next login assigns the role again. To sync roles only the first time a user signs in, return early when the user already exists in Permit:

const session = await getSession(req, res);
// check if user exists in permit
const user = await permit.api.getUser(session?.user?.sub);
if (user) {
console.log("user exists");
redirect(res, 302, "/");
return { props: {} };
}

With this check, role changes you make later in Auth0 don't reach Permit through the login sync. Choose one system to own role changes after the first login. See Where to manage roles.

6. Verify the integration

This test uses the roles from the demo app: manager can get and post tasks, and only admin can delete them.

  1. In Auth0, create a user and assign the manager role.
  2. Sign in to your app with that user. In the Permit dashboard, open Directory. The user appears with the Auth0 user ID as its key and the manager role.
  3. Create a task. The request succeeds.
  4. Delete a task. The API returns HTTP 403.
  5. In the Policy Editor, allow delete on task for manager and save. Or, in Directory, assign the admin role to the user.
  6. Delete a task again. The request succeeds without signing out and in again.

Remove a user from Permit

When you delete a user from your app, delete the user from both Auth0 and Permit. Add a Permit delete call to your remove user function, with the same user key you synced:

const removedUser = await permit.api.deleteUser(userObj.key);

Next steps