Skip to main content

Permit and Cognito Demo

Run a demo app that signs users in with Amazon Cognito and uses Permit.io to decide which of two actions each user can take. This page is for developers who want a working example before they add Permit to their own Cognito app. To add the integration to your own application, follow Cognito and Permit integration.

Patch the sync route before you run the demo

The POST /api/sync route in standalone_be/index.mjs of the permitio/cognito-integration repository declares payload with const inside its try block and reads it after the block, so the call to permit.api.syncUser() throws a ReferenceError and no user reaches Permit. The route also reads name and email from a Cognito access token, which carries neither claim. Apply the corrected route in 2. Run the demo application before you start the server, or the demo signs users in and then fails every permission check, because a user who doesn't exist in Permit has no roles.

The demo is based on the cognito-auth-example repository and the article How to add Cognito login to a website. The full code is in the permitio/cognito-integration repository.

What the demo app contains

FolderWhat it runs
frontend/A vanilla JavaScript page with a Login button and two action buttons. It signs the user in with the Cognito hosted login page and calls the backend.
standalone_be/An Express server on port 8000. It verifies Cognito tokens, syncs users to Permit, and protects the POST /say_hello and POST /wave_hello routes with permit.check().

The demo needs no policy decision point (PDP) of your own. standalone_be/index.mjs creates the Permit client with the address of the managed Cloud PDP, https://cloudpdp.api.permit.io.

Prerequisites

  • An AWS account with an Amazon Cognito user pool and an app client. The app client needs the hosted login page enabled, no client secret (the browser exchanges the code without one), and http://localhost:8181 as an allowed callback URL.
  • A Permit.io account and your environment API key. See Get your API key.
  • Node.js and npm.

1. Create the policy in Permit

Create the policy in the Permit dashboard, with the Permit API, or with an SDK (Python, Node.js, or another SDK):

  1. Create a resource with the key hello and the actions say and wave.
  2. Create the roles admin and viewer.
  3. Allow say and wave for admin. Allow only wave for viewer. You can also use the assign permissions API.

2. Run the demo application

  1. Clone the demo application:
git clone https://github.com/permitio/cognito-integration
  1. Open standalone_be/index.mjs and replace the POST /api/sync route with this corrected version, which keeps payload in scope after the try block, stops on an invalid token, and syncs only the sub claim that a Cognito access token carries:
// /standalone_be/index.mjs

app.post("/api/sync", async (req, res) => {
// payload is declared outside the try block, so it is still in scope below
let payload;
try {
payload = await verifier.verify(
req.headers.authorization?.split(" ")[1] // the JWT as string
);
} catch (error) {
res.status(403).send("Token not valid!");
return;
}
// the Cognito access token carries sub, but no name or email claim
const syncUser = await permit.api.syncUser({
key: payload.sub,
});
// To assign a role here, map Cognito groups to Permit roles and call:
// await permit.api.users.assignRole({
// user: payload.sub,
// role: caseSensitiveRoleKey,
// tenant: caseSensitiveTenantKey, // if you don't use tenants, use 'default'
// });
res.status(200).send(syncUser);
});
  1. In the standalone_be folder, create a .env file based on .env.example, with your Cognito and Permit values:
VariableValue
USER_POOL_IDThe ID of your Cognito user pool
CLIENT_IDThe ID of your Cognito app client
PERMIT_TOKENYour Permit environment API key
USER_POOL_ID=<your_user_pool_id>
CLIENT_ID=<your_app_client_id>
PERMIT_TOKEN=<permit_api_key>

To copy your Permit API key, open the Projects page, click the three dots on your environment, and select Copy API Key.

Permit Projects page with the Copy API Key option in the environment menu

  1. In the standalone_be folder, run npm install, then node index.mjs. The server logs Server listening on port 8000.

  2. In the frontend folder, copy config.example.js to config.js. Set cognitoLoginUrl to your Cognito domain, in the form https://<cognito-name>.auth.<region>.amazoncognito.com, and clientId to your app client ID.

  3. Serve the frontend folder on port 8181 with any static file server, for example npx http-server -p 8181. The backend allows browser requests only from http://localhost:8181.

3. Verify the demo

  1. Open http://localhost:8181. With no Cognito tokens in the browser's local storage, frontend/app.mjs sends the browser to the Cognito hosted login page without waiting for a click on Login. Sign up or sign in there. Cognito returns the browser to http://localhost:8181 with a code query parameter, the page exchanges the code for tokens, and it calls POST /api/sync.
  2. In the Permit dashboard, open Directory. A user appears whose key is the Cognito sub value of the account you signed in with. Assign the viewer role in the default tenant.
  3. In the demo app, click the first Say hello (if you have permission) button, which calls POST /say_hello. The browser shows an alert with Failed to say hello, because viewer can't say.
  4. Click the second button, which carries the same label and calls POST /wave_hello. The alert shows Waved hello.
  5. In Permit, assign the admin role to the user. Click the first button again. The alert shows Said hello.
Both action buttons carry the same label

frontend/index.html labels both action buttons Say hello (if you have permission). The first button in the page calls /say_hello and the second calls /wave_hello, so go by position, not by label.

A successful sync logs one line in the browser console:

User synced

If no user appears in step 2, open the browser console. A 403 response from /api/sync means the Cognito access token didn't verify, so check USER_POOL_ID and CLIENT_ID in standalone_be/.env. If the request to /api/sync never completes, the route still holds the unpatched code: the server runs Express 4, which doesn't turn a rejected async handler into a response, so the ReferenceError leaves the request open and prints an unhandled rejection in the server terminal.

4. How the demo app works

Cognito sign-in with the authorization code flow

On the frontend, the Login button calls redirectToLogin. The function sends the user to the Cognito hosted login page with the authorization code flow and PKCE (Proof Key for Code Exchange), which lets a browser app exchange the code without a client secret:

// app.mjs
const redirectToLogin = async () => {
const state = await generateNonce();
const codeVerifier = await generateNonce();
sessionStorage.setItem(`codeVerifier-${state}`, codeVerifier);
const codeChallenge = base64URLEncode(await sha256(codeVerifier));
window.location = `${cognitoLoginUrl}/login?response_type=code&client_id=${clientId}&state=${state}&code_challenge_method=S256&code_challenge=${codeChallenge}&redirect_uri=${window.location.origin}`;
};

//...

On the backend, the GET /api/id and GET /api/access routes verify the ID token and the access token with CognitoJwtVerifier from the aws-jwt-verify package, and return the token payload. The demo uses them to show the decoded tokens in its token table:

// standalone_be/index.mjs
// init CognitoJwtVerifier
const verifierIdToken = CognitoJwtVerifier.create({
userPoolId: userPoolId,
tokenUse: "id",
clientId: clientId,
});

app.get("/api/id", async (req, res) => {
try {
const payload = await verifierIdToken.verify(
req.headers.authorization?.split(" ")[1] // the JWT as string
);
res.status(200).send(payload);
} catch {
res.status(403).send("Token not valid!");
}
});
//...

When the demo syncs users to Permit

Permit can only match a user to roles after the user exists in Permit. After Cognito redirects back with a code query parameter, the frontend exchanges the code for tokens and calls the backend sync route with the access token:

// /frontend/app.mjs
//...
if (searchParams.get("code") !== null) {
//... get the tokens from Cognito
const syncUser = await fetch("http://localhost:8000/api/sync", {
method: "POST",
headers: new Headers({ Authorization: `Bearer ${tokens.access_token}` }),
});
//...
}

The backend route verifies the access token and calls permit.api.syncUser() with the Cognito sub claim as the user key. It assigns no roles, so assign them in the Permit dashboard, or add a permit.api.users.assignRole() call as the comment in the corrected route shows. For the general pattern and where to run it in your own app, see Sync Cognito users with Permit after login.

How the demo checks permissions

Authorization runs only on the backend. The POST /say_hello and POST /wave_hello routes verify the ID token, then call permit.check() with the user's sub as the user key, the action (say or wave), and hello as the resource. A route answers HTTP 200 only when the token verifies and Permit allows the action, and HTTP 403 otherwise. For the same pattern written for your own routes, see Check permissions on the backend.

Next steps