Skip to main content

Protect API routes

Overview

You can choose between three different methods to check for a session inside an API route handler. The easiest way to do it is to use the Verify Session middleware. Also, depending on your use case, you can directly fetch the session or manually verify the JWT. Check each method to see which one works for you.

Before you start

This guide only applies to scenarios which involve SuperTokens Session Access Tokens.

Using Verify Session

This function acts as a middleware inside your API endpoints. Hence, it requires that your backend framework supports the concept of middlewares. Besides checking for a session, it also writes responses to the client on its own, based on the session's validity and the provided configuration.

import express from "express";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express";

let app = express();

app.post("/like-comment", verifySession(), (req: SessionRequest, res) => {
let userId = req.session!.getUserId();
//....
});

Optional session verification

To make an API endpoint accessible even if there is no session update the middleware call to mark the session as not required.

import express from "express";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express";

let app = express();

app.post("/like-comment",
verifySession({sessionRequired: false}),
(req: SessionRequest, res) => {
if (req.session !== undefined) {
let userId = req.session.getUserId();
} else {
// user is not logged in...
}
}
);

Verify the claims of a session

To check if there are certain claims in the session as part of the verification process you can override the session validators. For example, you may want to check that the session has the admin role claim for certain APIs, or that the user has completed MFA, multi-factor authentication. You can achieve this by including the user role claim validator in the middleware global validators option. The global validators represent other validators that apply to all API routes by default. This may include things like a validator that ensures that the user's email is verified.

import { verifySession } from "supertokens-node/recipe/session/framework/express";
import express from "express";
import { SessionRequest } from "supertokens-node/framework/express";
import UserRoles from "supertokens-node/recipe/userroles";

let app = express();

app.post(
"/update-blog",
verifySession({
overrideGlobalClaimValidators: async (globalValidators) => [
...globalValidators,
UserRoles.UserRoleClaim.validators.includes("admin"),
// UserRoles.PermissionClaim.validators.includes("edit")
],
}),
async (req: SessionRequest, res) => {
// All validator checks have passed and the user is an admin.
}
);
Feature

You can also build your own custom claim validators based on your app's requirements.

Using Get Session

The Get Session function does the same thing as the middleware, but it does not write to the client on its own. It throws errors that you can catch and handle. If these errors remain unhandled, the SuperTokens error handler catches these errors and writes to the client (like the verifySession middleware).

You should use this function if your framework does not support middlewares or if you want additional control over error management.

import express from "express";
import Session from "supertokens-node/recipe/session";

let app = express();

app.post("/like-comment", async (req, res, next) => {
try {
let session = await Session.getSession(req, res);

let userId = session.getUserId();
//....
} catch (err) {
next(err);
}
});

Optional session verification

To make an API endpoint accessible even if there is no session update the middleware call to mark the session as not required.

import express from "express";
import Session from "supertokens-node/recipe/session";

let app = express();

app.post("/like-comment", async (req, res, next) => {
try {
let session = await Session.getSession(req, res, { sessionRequired: false })

if (session !== undefined) {
let userId = session.getUserId();
} else {
// user is not logged in...
}
//....
} catch (err) {
next(err);
}
});

Verify the claims of a session

To check if there are certain claims in the session as part of the verification process you can override the session validators. For example, you may want to check that the session has the admin role claim for certain APIs, or that the user has completed MFA, multi-factor authentication. This can be achieved by including the user role claim validator in the middleware global validators option. The global validators represent other validators that apply to all API routes by default. This may include things like a validator that ensures that the user's email is verified.

import express from "express";
import Session from "supertokens-node/recipe/session";
import UserRoles from "supertokens-node/recipe/userroles";

let app = express();

app.post("/like-comment", async (req, res, next) => {
try {
let session = await Session.getSession(req, res, {
overrideGlobalClaimValidators: async (globalValidators) => [
...globalValidators,
UserRoles.UserRoleClaim.validators.includes("admin"),
// UserRoles.PermissionClaim.validators.includes("edit")
]
});

let userId = session.getUserId();
//....
} catch (err) {
next(err)
}
});
feature

You can also build your own custom claim validators based on your app's requirements.

Build your own middleware

Both these functions perform session verification. However, Verify Session is a middleware that returns a reply directly to the frontend if the input access token is invalid or expired. On the other hand, Get Session is a function that returns a session object on successful verification. It throws an exception that you can handle if the access token expires or is invalid.

Internally, Verify Session uses Get Session in the following way:

import { VerifySessionOptions } from "supertokens-node/recipe/session/types";
import { errorHandler } from "supertokens-node/framework/express";
import { NextFunction, Request, Response } from "express";
import Session from "supertokens-node/recipe/session";
import { Error as SuperTokensError } from "supertokens-node";

function verifySession(options?: VerifySessionOptions) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
(req as any).session = await Session.getSession(req, res, options);
next();
} catch (err) {
if (SuperTokensError.isErrorFromSuperTokens(err)) {
if (err.type === Session.Error.TRY_REFRESH_TOKEN) {
// This means that the session exists, but the access token
// has expired.

// You can handle this in a custom way by sending a 401.
// Or you can call the errorHandler middleware as shown below
} else if (err.type === Session.Error.UNAUTHORISED) {
// This means that the session does not exist anymore.

// You can handle this in a custom way by sending a 401.
// Or you can call the errorHandler middleware as shown below
} else if (err.type === Session.Error.INVALID_CLAIMS) {
// The user is missing some required claim.
// You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend.
}

// OR you can use this errorHandler which will
// handle all of the above errors in the default way
errorHandler()(err, req, res, (err) => {
next(err)
})
} else {
next(err)
}
}
};
}

The errorHandler sends a 401 reply to the frontend if the getSession function throws an exception indicating that the session does not exist or if the access token has expired.

Get the session using the Access Token

In the above snippets, Get Session requires the request object and, depending on your backend language and framework, may also require the response object. Either way, this version of Get Session automatically reads from the request. And automatically sets the response based on the update to the session tokens. Whilst this is convenient, sometimes, you may not have the request or response objects, or you may not want SuperTokens to set the tokens in the response automatically. In this case, you can use the getSessionWithoutRequestResponse function.

This function works similarly to getSession, except that it doesn't depend on the request or response objects. It's your responsibility to provide this function the access token. You must write the update tokens to the response if the tokens update during this API call.

import { VerifySessionOptions } from "supertokens-node/recipe/session/types";
import { SessionContainer } from "supertokens-node/recipe/session";
import Session from "supertokens-node/recipe/session";
import { Error as SuperTokensError } from "supertokens-node";

async function verifySession(accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions) {
let session: SessionContainer | undefined;
try {
session = await Session.getSessionWithoutRequestResponse(accessToken, antiCsrfToken, options);
} catch (err) {
if (SuperTokensError.isErrorFromSuperTokens(err)) {
if (err.type === Session.Error.TRY_REFRESH_TOKEN) {
// This means that the session exists, but the access token
// has expired.

// You can handle this in a custom way by sending a 401.
// Or you can call the errorHandler middleware as shown below
} else if (err.type === Session.Error.UNAUTHORISED) {
// This means that the session does not exist anymore.

// You can handle this in a custom way by sending a 401.
// Or you can call the errorHandler middleware as shown below
} else if (err.type === Session.Error.INVALID_CLAIMS) {
// The user is missing some required claim.
// You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend.
}
}
throw err;
}
if (session !== undefined) {
// we can use the `session` container as we usually do..
// TODO: API logic...

// At the end of the API logic, we must fetch all the tokens from the session container
// and set them in the response headers / cookies ourselves.
const tokens = session.getAllSessionTokensDangerously();
if (tokens.accessAndFrontTokenUpdated) {
// TODO: set access token in response via tokens.accessToken
// TODO: set front-token in response via tokens.frontToken
if (tokens.antiCsrfToken) {
// TODO: set anti-csrf token update in response via tokens.antiCsrfToken
}
}
}
}

Using a JWT verification library

If the previous methods are not suitable for your use case, you can use validate the token manually. This method works for cases like:

  • Your APIs are on a backend for which SuperTokens doesn't have SDKs.
  • You are using a non http protocol (like websockets) and passing in the access token.
  • You are using an API gateway which does JWT verification based on the JWKs endpoint.
caution

The downside to using JWT verification manually is that:

  • Pick and configure a JWT verification library for your framework. Many online guides explain how to do this.
  • You need to manually verify some custom claims in the JWT (like the user's role is admin, or that the user's email is verified) based on your authorization rules.
  • You don't have access to the session object using which you can modify the session's access token payload, or revoke the session. These operations can occur in an offline manner, but they reflect in the user's session only after a session refresh.

The manual session verification method should work in this way:

1Verify the JWT signature and expiry using a JWT verification library
2Check for custom claim values for authorization.
3Prevent cross-site request forgery (CSRF) attacks in case you are using cookies to store the JWT.

App Info

Adjust these values based on the application that you are trying to configure. To learn more about what each field means check the references page.
This is the URL of your app's API server.
This is the URL of your app's API server.
SuperTokens will expose it's APIs scoped by this base API path.
This is the URL of your website.
The path where the login UI will be rendered

With the JSON Web Key Set (JWKS) Endpoint

Some libraries let you provide a JSON Web Key Set (JWKS) endpoint to verify a JWT. The JSON Web Key Set (JWKS) endpoint exposed by SuperTokens is available at the following URL:

curl --location --request GET '<YOUR_API_DOMAIN>/auth/jwt/jwks.json'

Below is an example for NodeJS showing how you can use jsonwebtoken and jwks-rsa together to achieve JWT verification using the jwks.json endpoint.

import JsonWebToken, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

var client = jwksClient({
jwksUri: '<YOUR_API_DOMAIN>/auth/jwt/jwks.json'
});

function getKey(header: JwtHeader, callback: SigningKeyCallback) {
client.getSigningKey(header.kid, function (err, key) {
var signingKey = key!.getPublicKey();
callback(err, signingKey);
});
}

let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header
JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) {
let decodedJWT = decoded;
// Use JWT
});

With the public key string

caution

This method is less secure compared to the first method because it disables key rotation of the access token signing key. In this case, if the private key is somehow stolen, it can be used indefinitely to forge access tokens (Unless you manually change the key in the database).

Some JWT verification libraries require you to provide the JWT secret / public key for verification. You can obtain the JWT secret from SuperTokens in the following way:

1Query the JWKS.json endpoint:

curl --location --request GET '<YOUR_API_DOMAIN>/auth/jwt/jwks.json'

{
"keys": [
{
"kty": "RSA",
"kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86",
"n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=",
"e": "AQAB",
"alg": "RS256",
"use": "sig"
},
{
"kty": "RSA",
"kid": "d-230...802340",
"n": "AMZruthvYz7...lx0rU8c=",
"e": "...",
"alg": "RS256",
"use": "sig"
}
]
}
important

The above shows an example output which returns two keys. More keys could return based on the configured key rotation setting in the core. If you notice, each key's kid starts with a s-.. or a d-... The s-.. key is a static key that never changes, whereas d-... keys are dynamic keys that keep changing. If you are hard-coding public keys somewhere, you always want to pick the s-.. key.

One exception is that if you see a key with kid that doesn't start with s- or with d-, then treat that as a static key. This only happens if you used to run an older SuperTokens core that was less than version 5.0.

2Run the NodeJS script below to convert the above output to a PEM file format.

import jwkToPem from 'jwk-to-pem';

// This JWK is copied from the result of the above SuperTokens core request
let jwk = {
"kty": "RSA",
"kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86",
"n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=",
"e": "AQAB",
"alg": "RS256",
"use": "sig"
};

let certString = jwkToPem(jwk);

The above snippet would generate the following certificate string:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxmu62G9jPsW34OnQEL9I
QQlpYr3Wz9gD5FHzWIsnoFNPqAmnQJxXgN8HKcVT/n11EY5nJVCkBboOudz/ovJm
... (truncated for display)
XhfWeIqBjrJheltfqKzgcJ+Fh91ptwTnbFgw2X6DD1cOOwjF8ExQO84VEdaXHStT
xwIDAQAB
-----END PUBLIC KEY-----

Use the generated Privacy-Enhanced Mail (PEM) string in your code as shown below:

import JsonWebToken from 'jsonwebtoken';

// Truncated for display
let certificate = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxmu62G9jPsW34OnQEL9IQQlpYr3Wz9gD5FHzWIsnoFNPqAmnQJxXgN8HKcVT/n11EY5nJVCkBboOudz/ovJm...XhfWeIqBjrJheltfqKzgcJ+Fh91ptwTnbFgw2X6DD1cOOwjF8ExQO84VEdaXHStTxwIDAQAB\n-----END PUBLIC KEY-----";
let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header
JsonWebToken.verify(jwt, certificate, function (err, decoded) {
let decodedJWT = decoded;
// Use JWT
});
3Tell SuperTokens to always only use the static key when creating a new session.

You can accomplish this by setting the below configuration in the backend SDK:

import SuperTokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";

SuperTokens.init({
supertokens: {
connectionURI: "...",
},
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
recipeList: [
Session.init({
useDynamicAccessTokenSigningKey: false,
})
]
});
caution

Updating this value causes a spike in the session refresh API, as and when users visit your application.

Check for custom claim values for authorization

Once you have verified the access token, you can fetch the payload and perform authorization checks based on the values of the custom claims. For example, if you want to check if the user's email is verified, you should check the st-ev claim in the payload as shown below:

import JsonWebToken, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

var client = jwksClient({
jwksUri: '<YOUR_API_DOMAIN>/auth/jwt/jwks.json'
});

function getKey(header: JwtHeader, callback: SigningKeyCallback) {
client.getSigningKey(header.kid, function (err, key) {
var signingKey = key!.getPublicKey();
callback(err, signingKey);
});
}

let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header
JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) {
if (err) {
// send a 401 to the frontend..
}
if (decoded !== undefined && typeof decoded !== "string") {
let isEmailVerified = (decoded as any)["st-ev"].v
if (!isEmailVerified) {
// send a 403 to the frontend..
}
}
});

Claims like email verification and user roles claims are included in the access token by the backend SDK automatically. You can even add your own custom claims to the access token payload, and those claims will be in the JWT as expected.

important

On claim validation failure, you must send a 403 to the frontend, which causes the frontend SDK (pre-built UI SDK) to recheck the claims added on the frontend and navigate to the right screen.

Check for anti-csrf during authorization

important

You need to check for anti-cross-site request forgery (CSRF) for non GET requests when cookie-based authentication is active.

Two methods exist for configuring cross-site request forgery (CSRF) protection: VIA_CUSTOM_HEADER and VIA_TOKEN.

VIA_CUSTOM_HEADER

VIA_CUSTOM_HEADER is automatically set if sameSite is none or if your apiDomain and websiteDomain do not share the same top level domain. In this case, you need to check for the presence of the rid header from incoming requests.

VIA_TOKEN

When configured with VIA_TOKEN, an explicit anti-csrf token attaches as a header to requests with anti-csrf as the key. To verify the anti-csrf token, you need to compare it to the value of the antiCsrfToken key from the payload of the decoded JWT.