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.
}
);
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)
}
});
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 (likewebsockets
) and passing in the access token. - You are using an API gateway which does JWT verification based on the JWKs endpoint.
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:
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.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
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:
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"
}
]
}
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
.
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
});
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,
})
]
});
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.
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
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.