Cognito and Permit Integration
Add Permit.io authorization to an application that signs users in with Amazon Cognito. You create a policy in Permit, sync each Cognito user to Permit after login, and call permit.check() before each protected backend action. This guide is for developers with a Cognito user pool and a Node.js backend. The examples come from the Cognito demo application, a vanilla JavaScript frontend with an Express backend.
Amazon Cognito authenticates users and issues JSON Web Tokens (JWTs). Permit stores your authorization policy, and a policy decision point (PDP) evaluates each permission check against it.
Prerequisites
- An AWS account with a Cognito user pool and an app client.
- A frontend that sends users who aren't signed in to the Cognito login page.
- A Permit.io account and your environment API key. See Get your API key.
- The
permitioandaws-jwt-verifynpm packages installed in your backend.
1. Create the policy in Permit
Create the roles, resources, and permissions your application checks. You can use the Permit dashboard, the Permit API, or one of the Permit SDKs. For help designing roles, read the Permit blog post Planning your app's RBAC.
- Create roles. Create the roles your application needs, for example
admin,editor, andviewer. If you map Cognito groups to roles, use the same keys. Role keys are case-sensitive. - Create resources. Create a resource for each part of your application you protect, with the actions users take on it. For example, a task list needs a
taskresource withcreate,get, anddeleteactions. See the Policy Editor resources tab or the create resource API. - Assign permissions. In the Policy Editor, check the actions each role can take on each resource, or use the assign permissions API.
2. Sync Cognito users with Permit after login
Permit can only match a user to roles after the user exists in Permit. Sync the user right after login. You can set the Cognito redirect URL to a page that runs the sync, or run the sync when your frontend sees the code query parameter that Cognito adds after login.
The demo app uses the code parameter. After it exchanges the code for tokens, the frontend calls a backend sync route and sends a Cognito token in the Authorization header:
// frontend/app.mjs
if (searchParams.get("code") !== null) {
//...
const syncUser = await fetch("api/sync", {
method: "POST",
headers: new Headers({ Authorization: `Bearer ${tokens.id_token}` }),
});
//...
}
The backend verifies the token, then syncs the user with the Cognito sub claim as the user key:
// standalone_be/index.mjs
// init Cognito verifier and Permit SDK
import { CognitoJwtVerifier } from "aws-jwt-verify";
import { Permit } from "permitio";
// verifier for the id token:
const verifierIdToken = CognitoJwtVerifier.create({
userPoolId: "[your user pool id]",
tokenUse: "id",
clientId: "[your client id]",
}
);
const permit = new Permit(
{
token: process.env.PERMIT_API_KEY, // [your permit token]
// in production, you might need to change this url to fit your deployment
pdp: "https://cloudpdp.api.permit.io",
}
);
// cognitoUser example:
//{
// "at_hash": "dttAg8DUmRrf_xxxxxxxxx",
// "sub": "e69bd1ef-0067-40ef-9262-xxxxxxxxx",
// "email_verified": true,
// "iss": "https://cognito-idp.us-east-2.amazonaws.com/us-east-xxxxxxxxx",
// "cognito:username": "test_username",
// "origin_jti": "ce6914d5-5cd2-40d4-a0c3-xxxxxxxxx",
// "aud": "27fr56i7g292frkxxxxxxxxx",
// "token_use": "id",
// "auth_time": 1689097274,
// "name": "test_name",
// "exp": 1689100874,
// "iat": 1689097274,
// "jti": "1345944f-d8a8-4301-80f9-xxxxxxxxx",
// "email": "test@permit.io"
//}
// sync user route
app.post("/api/sync", async (req, res) => {
let cognitoUser;
try {
cognitoUser = await verifierIdToken.verify(
req.headers.authorization?.split(" ")[1] // the JWT as string
);
} catch (error) {
res.status(403).send("Token not valid!");
return;
}
const syncUser = await permit.api.syncUser({
"first_name": cognitoUser.name,
"key": cognitoUser.sub,
"email": cognitoUser.email,
}
);
// you can also assign role to a user here if you have mapping between Cognito groups and Permit roles
// with the assign role SDK method
// await permit.api.assignRole({
// "user": cognitoUser.sub,
// "role": caseSensitiveRoleKey,
// "tenant": caseSensitiveTenantKey, // if you don't use tenants, use 'default'
// });
res.status(200).send(syncUser);
}
);
Replace these placeholders in the backend code:
| Placeholder | Value | Where to find it |
|---|---|---|
[your user pool id] | Your Cognito user pool ID, as a string | Amazon Cognito > User pools > your user pool |
[your client id] | Your Cognito app client ID, as a string | Amazon Cognito > User pools > your user pool > App clients > your app client |
PERMIT_API_KEY environment variable | Your Permit environment API key | Get your API key |
Keep the Permit API key in an environment variable instead of writing it in the file. Anyone with the key can change your environment's policy through the Permit API.
The route reads first_name and email from the verified token. Cognito puts the name and email claims in the ID token, not in the access token. The commented cognitoUser example in the code is an ID token payload.
To assign roles during the sync, for example from a mapping between Cognito groups and Permit roles, call permit.api.assignRole() with the user key, role key, and tenant key. See the Node.js assignRole reference.
3. Check permissions on the backend
Call permit.check() before each action you protect, such as each create, read, update, and delete route in your API. Pass the Cognito sub as the user key, the action, and the resource. Return HTTP 403 when Permit denies the action:
// An example of a protected route
app.delete("/api/tasks/:id", async (req, res) => {
const cognitoUser = await verifierIdToken.verify(
req.headers.authorization?.split(" ")[1] // the JWT as string
);
const permitted = await permit.check(
cognitoUser.sub, // the user key from the syncUser step
'delete', // the action name
'task' // the resource name
);
if (!permitted) {
res.status(403).send('Not permitted');
return;
}
// delete the task
//...
});
permit.check() returns a promise that resolves to true or false. Await it inside an async route handler before you read the result.
4. Verify the integration
- Sign in to your app with a Cognito user.
- In the Permit dashboard, open Directory. The user appears with the Cognito
subas its key. Assign a role if the sync didn't assign one. - Call a route for an action the role allows. The route runs.
- In the Policy Editor, remove that permission from the role and save.
- Call the same route again. The route returns HTTP 403. You don't need to sign in again, because the backend checks Permit on every request.
Remove a user from Permit
When you delete a user from your app, delete the user from both Cognito and Permit. Add a Permit delete call to your remove user function, and pass the user key you synced (the Cognito sub):
const removedUser = await permit.api.deleteUser(cognitoUser.sub);
Next steps
- Cognito demo application: run the working example.
- Deploy the PDP to production: run a container PDP next to your backend instead of the Cloud PDP.
- Check permissions: pass tenants, resource instances, and attributes to
permit.check(). - How authentication connects to Permit.io: sync methods and role ownership models.