Skip to main content
Which frontend SDK do you use?
supertokens-web-js / mobile
supertokens-auth-react

Option 2. Using claim validators

What are session claims?#

SuperTokens session has a property called accessTokenPayload. This is a JSON object that's stored in a user's session which can be accessed on the frontend and backend. The key-values in this JSON payload are called claims.

What are session claim validators?#

Session claim validators check if the the claims in the session meet a certain criteria before giving access to a resource.

Let's take two examples:

  • 2FA session claim validator: This validator checks if the session claims indicates that the user has completed both the auth factors or not.
  • Email verification claim validator: This checks if the user's session indicates if they have verified their email or not.

In either case, the claim validators base their checks on the claims (or properties) present in the session's access token payload. These claims can be added by you or by the SuperTokens SDK (ex, the user roles recipe adds the roles claims to the session).

This document will guide you through how to use prebuilt session claims and session claims validators as well as how to build your own.

Why do we need session claim validators?#

The claims in the payload represent the state of the user's access properties. Session claim validators ensure that the state is up to date when the claims are being checked.

For example, if during sign in, the user has the role of "user", this will be added to their session by SuperTokens. If during the course of their session, the user is upgraded to an "admin" role, then the session claim needs to be updated to reflect this as well. To do this automatically, the claim validator for roles will auto refresh the role in the session periodically. You can even specify that you want to force refresh the state when using the validator in your APIs.

Without a special construct of session claim validators, the updating of the session claims would have to be done manually by you (the developer), so to save you the time and effort, we introduced this concept.

Session claim interface#

On the backend#

Before we dive deep into claim validators, let's talk about session claim objects. These are objects that conform to an interface that allows SuperTokens to automatically add session claims to the access token payload. Here is the interface:

interface SessionClaim<T> {
constructor(public readonly key: string) {}

fetchValue(userId: string, userContext: any): Promise<T | undefined>;

addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject;

removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject;

removeFromPayload(payload: JSONObject, userContext?: any): JSONObject;

getValueFromPayload(payload: JSONObject, userContext: any): T | undefined;
}
  • T represents a generic type. For a boolean claim (for example if the email is verified or not), the type of T is a boolean.

  • fetchValue is responsible for fetching the value of the claim from its source. For example, the email verification claim uses the EmailVerification.isEmailVerified function from the email verification recipe to return a boolean from this function.

  • addToPayload_internal function is responsible for adding the claim value to the input payload and returning the modified payload. The payload here represents the access token's payload. Some of the in built claims in the SDK modify the payload in the following way:

    {
    ...payload,
    "<key>": {
    "t": <current time in milliseconds>,
    "v": <value>
    }
    }

    The key variable is an input to the constructor. For the in built email verification claim, the value of key is "st-ev".

  • removeFromPayloadByMerge_internal function is responsible for modifying the input payload to remove the claim in such a way that if mergeIntoAccessTokenPayload is called, then it would remove that claim from the payload. This usually means modifying the payload like:

    {
    ...payload,
    "<key>": null
    }
  • removeFromPayload function is similar to the previous function, except that it deletes the key from the input payload entirely.

  • getValueFromPayload function is supposed to return the claim's value given the input payload. For the in built claims, it's usually payload[<key>][v] or undefined of the key doesn't exist in the payload.

The SDK provides a few base claim classes which make it easy for you to implement your own claims:

  • PrimitiveClaim: Can be used to add any primitive type value (boolean, string, number) to the session payload.
  • PrimitiveArrayClaim: Can be used to add any primitive array type value (boolean[], string[], number[]) to the session payload.
  • BooleanClaim: A special case of the PrimitiveClaim, used to add a boolean type claim.

Using these, we have built a few useful claims:

  • EmailVerificationClaim: This is used to store info about if the user has verified their email.
  • RolesClaim: This is used to store the list of roles associated with a user.
  • PermissionClaim: This is used to store the list of permissions associated with the user.

You can image all sorts of claims that can be built further:

  • If the user has completed 2FA or not
  • If the user has filled in all the profile info post sign up or not
  • The last time the user authenticated themselves (useful for if you want to ask the user for their password after a certain time period).

On the frontend#

Just like the backend, the frontend also has the concept of Session claim objects which need to confirm to the following interface:

type SessionClaim<T> = {
refresh(): Promise<void>;

getValueFromPayload(payload: any): T | undefined;

getLastFetchedTime(payload: any): number | undefined;
};
  • The refresh function is responsible for refreshing the claim values in the session via an API call. The API call is expected to update the claim values if required.

  • getValueFromPayload helps with reading the value from the session claim.

  • getLastFetchedTime reads the claim to return the timestamp (in milliseconds) of the last time the claim was refreshed.

When used, these objects provide a way for the SuperTokens SDK to update the claim values as and when needed. For example, in the built-in email verification claim, the refresh function calls the backend API to check if the email is verified. That API in turn updates the session claim to reflect the email verification status. This way, even if the email was marked as verified in offline mode, the frontend will be able to get the email verification status update automatically.

Just like the backend SDK, the frontend SDK also exposes a few base claims:

Claim validator interface#

On the backend#

Once a claim is added to the session, we must specify the checks that need to run on them during session verification. For example, if we want an API to be guarded so that only admin roles can access them, we need a way to tell SuperTokens to do that check. This is where claim validators come into the picture. Here is the shape for a claim validator object:

type SessionClaimValidator {
id: string,

claim: SessionClaim<any>,

shouldRefetch: (payload: any, userContext: any) => Promise<boolean>,

validate: (payload: any, userContext: any) => Promise<ClaimValidationResult>;
}

type ClaimValidationResult = { isValid: true } | { isValid: false; reason?: {...} };
  • The id is used to identify the session claim validator. This is useful to know which validator failed in case several of them are being checked at the same time. The value of this is usually the same as the claim object's key, but it can be set to anything else.

  • The claim property is a reference to the claim object that's associated with this validator. The shouldRefetch and validate functions will use claim.getValueFromPayload to fetch the claim value from the input payload.

  • shouldRefetch is a function which determines if the value of the claim should be fetched again. In the in built validators, this function usually returns true if the claim does not exist in the payload, or if it's too old.

  • validate function extracts the claim value from the input payload (usually using claim.getValueFromPayload), and determines if the validator check has passed or not. For example, if the validator is supposed to enforce that the user's email is verified, and if the claim value is false, then this function would return:

    {
    isValid: false,
    message: "wrong value",
    expectedValue: true,
    actualValue: false
    }

Using this interface and the claims interface, SuperTokens runs the following session claim validation process during session verification:

function validateSessionClaims(accessToken, claimValidators[]) {
payload = accessToken.getPayload();

// Step 1: refetch claims if required
foreach validator in claimValidators {
if (validator.shouldRefetch(payload)) {
claimValue = validator.claim.fetchValue(accessToken.userId)
payload = validator.claim.addToPayload_internal(payload, claimValue)
}
}

failedClaims = []

// Step 2: Validate all claims
foreach validator in claimValidators {
validationResult = validator.validate(payload)
if (!validationResult.isValid) {
failedClaims.push({id: validator.id, reason: validationResult.reason})
}
}

return failedClaims
}

The built-in base claims (PrimitiveClaim, PrimitiveArrayClaim, BooleanClaim) all expose a set of useful validators:

In all of the above claim validators, the maxAgeInSeconds input (which is optional) governs how often the session claim value should be refetched

  • A value of 0 will make it refetch the claim value each time a check happens.
  • If not passed, the claim will only be refetched if it's missing in the session. The in built claims like email verification or user roles claims have a default value of five mins, meaning that those claim values are refreshed from the database after every five mins.

On the frontend#

Just like the backend, the frontend too has session claim validators that conform to the following shape

class SessionClaimValidator {
constructor(public readonly id: string) {}

refresh(): Promise<void>;

shouldRefresh(accessTokenPayload: any): Promise<boolean> | boolean;

validate(
accessTokenPayload: any
): Promise<ClaimValidationResult> | ClaimValidationResult;
}

type ClaimValidationResult = { isValid: true } | { isValid: false; reason?: any };
  • The refresh function is the same as the one in the frontend claim interface.
  • shouldRefresh is function which determines if the claim should be checked against the backend before calling validate. This usually returns true if the claim value is too old or if it is not present in the accessTokenPayload.
  • The validate function checks the accessTokenPayload for the value of the claim and returns an appropriate response.

The logic for how validators are run on the frontend is the same as on the backend:

  • First we refresh the claim if that claim's shouldRefresh returns true (and we do this for all the claims)
  • Then we call the validate function on the claims one by one to return a array of validation result.

In case validation fails, you can choose to render a certain UI or redirect the user. In certain claim validators, like the email verification validator, the frontend SDK (for pre built UI) automatically redirects the user to the email verification screen.

And once again, just like in the backend, the frontend too exposes several helper functions like hasValue, includes, excludes, isTrue etc for the base claim classes.

How to add or modify a claim in a session?#

Once you have made your own session claim object, you need to add it to a session. There are two ways in which you can add them:

  • During session creation
  • Updating the session to add a claim after session creation

During session creation#

You need to override the createNewSession function to modify the access token payload like shown below:

import SuperTokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import { UserRoleClaim } from "supertokens-node/recipe/userroles";

SuperTokens.init({
supertokens: {
connectionURI: "...",
},
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
recipeList: [
// ...
Session.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
createNewSession: async function (input) {
let userId = input.userId;

// This goes in the access token, and is availble to read on the frontend.
input.accessTokenPayload = {
...input.accessTokenPayload,
...(await UserRoleClaim.build(input.userId, input.userContext))
};

/*
At this step, the access token paylaod looks like this:
{
...input.accessTokenPayload,
st-roles: {
v: ["admin"],
t: <current time in MS>
}
}
*/

return originalImplementation.createNewSession(input);
},
};
},
},
})
]
});

In the above code snippet, we take an example of manually adding the user roles claim to the session (note that this is done automatically for you if you initialise the user roles recipe).

The build function is a helper function which all claims have that does the following:

class Claim {
// other functions like fetchValue, getValueFromPayload etc..
function build(userId) {
claimValue = this.fetchValue(userId);
return this.addToPayload_internal({}, claimValue)
}
}

Post session creation#

Once you have the session container object (result of session verification), you can call the fetchAndSetClaim function on it to set the claim value in the session

import { SessionContainer } from "supertokens-node/recipe/session";
import { UserRoleClaim } from "supertokens-node/recipe/userroles";

async function addClaimToSession(session: SessionContainer) {
await session.fetchAndSetClaim(UserRoleClaim)
}

fetchAndSetClaim fetches the claim value using claim.fetchValue and adds it to the access token in the session.

There is also an offline version of fetchAndSetClaim exposed by the Session recipe which takes the claim and a sessionHandle. This will update that session's access token payload in the database, so when that session refreshes, the new access token will reflect that change.

Manually setting a claim's value in a session#

You can also manually set a claim's value in the session without it using the fetchValue function. This is useful for situations in which the fetchValue function doesn't read from a data source (ex: a database).

An example of this is the 2FA claim. The fetchValue function always returns false because there is no database entry that is updated when 2FA is completed - we simply update the session payload.

import { SessionContainer } from "supertokens-node/recipe/session";
import { BooleanClaim } from "supertokens-node/recipe/session/claims";

const SecondFactorClaim = new BooleanClaim({
fetchValue: () => false,
key: "2fa-completed",
});

async function mark2FAAsComplete(session: SessionContainer) {
await session.setClaimValue(SecondFactorClaim, true)
}

How to add a claim validator?#

In order for SuperTokens to check the claims during session verification, you need to add the claim validators in the backend / frontend SDK. On the backend, the claim validators will be run during session verification, and on the frontend, they will run when you use the <SessionAuth> component (for pre built UI), or when you call the Session.validateClaims function.

Adding a validator check globally#

This method allows you to add a claim validator such that it applies checks globally - for all your API / frontend routes. This is useful for claim validators like 2FA, when you want all users to be able to access the app only if they have completed 2FA. It also helps prevent development mistakes wherein someone may forget to explictly add the 2FA claim check for each route.

On the backend#

import SuperTokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import { BooleanClaim } from "supertokens-node/recipe/session/claims";

const SecondFactorClaim = new BooleanClaim({
fetchValue: () => false,
key: "2fa-completed",
});

SuperTokens.init({
supertokens: {
connectionURI: "...",
},
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
recipeList: [
// ...
Session.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getGlobalClaimValidators: async function (input) {
return [...input.claimValidatorsAddedByOtherRecipes, SecondFactorClaim.validators.isTrue()]
}
};
},
},
})
]
});

This will run the isTrue validator check on the SecondFactorClaim during each session verification and will only allow access to your APIs if this validator passes (i.e., the user has finished 2FA). If this validator fails, SuperTokens will send a 403 to the frontend.

note

The claim validators added this way do not run for the APIs exposed by the SuperTokens middleware.

On the frontend#

import SuperTokens from "supertokens-auth-react"
import Session, { BooleanClaim } from "supertokens-auth-react/recipe/session";

const SecondFactorClaim = new BooleanClaim({
id: "2fa-completed",
refresh: async () => {
// we do nothing here because refreshing the 2fa claim doesn't make sense
}
});

SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
recipeList: [
//...
Session.init({
override: {
functions: (oI) => {
return {
...oI,
getGlobalClaimValidators: function (input) {
return [...input.claimValidatorsAddedByOtherRecipes, SecondFactorClaim.validators.isTrue()]
}
}
}
}
})
]
});

Now whenever you wrap your component with the <SessionAuth> wrapper, SuperTokens will run the SecondFactorClaim.validators.isTrue() validator check automatically and provide you the result in the session context.

import Session, { BooleanClaim } from "supertokens-auth-react/recipe/session";

const SecondFactorClaim = new BooleanClaim({
id: "2fa-completed",
refresh: async () => {
// we do nothing here because refreshing the 2fa claim doesn't make sense
}
});

function Dashboard(props: any) {
let sessionContext = Session.useSessionContext();

if (sessionContext.loading) {
return null;
}

if (sessionContext.invalidClaims.some(i => i.validatorId === SecondFactorClaim.id)) {
// the 2fa check failed. We should redirect the user to the second
// factor screen.
return "You cannot access this page because you have not completed 2FA";
}

// 2FA check passed
}

Adding a validator check to a specific route#

If you want a session claim validator to run only on certain routes, then you should use this method of adding them.

To illustrate this, we will be taking an example of user roles claim validator in which we will be giving access to a user only if they have the "admin" role.

On the backend#

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.
}
);
  • We add the UserRoleClaim validator to the verifySession function which makes sure that the user has an admin role.
  • The globalValidators represents other validators that apply to all API routes by default. This may include a validator that enforces that the user's email is verified (if enabled by you).
  • We can also add a PermissionClaim validator to enforce a permission.

For more complex access control, you can even extract the claim value from the session and then check the value yourself:

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

let app = express();

app.post("/update-blog", verifySession(), async (req: SessionRequest, res) => {
const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim);

if (roles === undefined || !roles.includes("admin")) {
// this error tells SuperTokens to return a 403 to the frontend.
throw new STError({
type: "INVALID_CLAIMS",
message: "User is not an admin",
payload: [{
id: UserRoles.UserRoleClaim.key
}]
})
}
// user is an admin..
});

On the frontend#

Provide the overrideGlobalClaimValidators prop to the <SessionAuth> component as shown below

import React from "react";
import { SessionAuth, useSessionContext } from 'supertokens-auth-react/recipe/session';
import { UserRoleClaim, /*PermissionClaim*/ } from 'supertokens-auth-react/recipe/userroles';

const AdminRoute = (props: React.PropsWithChildren<any>) => {
return (
<SessionAuth
overrideGlobalClaimValidators={(globalValidators) =>
[...globalValidators,
UserRoleClaim.validators.includes("admin"),
/* PermissionClaim.validators.includes("modify") */
]
}
>
<InvalidClaimHandler>
{props.children}
</InvalidClaimHandler>
</SessionAuth>
);
}

function InvalidClaimHandler(props: React.PropsWithChildren<any>) {
let sessionContext = useSessionContext();
if (sessionContext.loading) {
return null;
}

if (sessionContext.invalidClaims.some(i => i.validatorId === UserRoleClaim.id)) {
return <div>You cannot access this page because you are not an admin.</div>
}

// We show the protected route since all claims validators have
// passed implying that the user is an admin.
return <div>{props.children}</div>;
}

Above we are creating a generic component called AdminRoute which enforces that its child components can only be rendered if the user has the admin role.

In the AdminRoute component, we use the SessionAuth wrapper to ensure that the session exists. We also add the UserRoleClaim validator to the <SessionAuth> component which checks if the validators pass or not.

Finally, we check the result of the validation in the InvalidClaimHandler component which displays "You cannot access this page because you are not an admin." if the UserRoleClaim claim failed.

If all validation passes, we render the props.children component.

note

You can extend the AdminRoute component to check for other types of validators as well. This component can then be reused to protect all of your app's components (In this case, you may want to rename this component to something more appropriate, like ProtectedRoute).

If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself:

import Session from "supertokens-auth-react/recipe/session";
import {UserRoleClaim} from "supertokens-auth-react/recipe/userroles"

function ProtectedComponent() {
let claimValue = Session.useClaimValue(UserRoleClaim)
if (claimValue.loading || !claimValue.doesSessionExist) {
return null;
}
let roles = claimValue.value;
if (roles !== undefined && roles.includes("admin")) {
// User is an admin
} else {
// User doesn't have any roles, or is not an admin..
}
}
caution

Unlike using the overrideGlobalClaimValidators prop, the useClaimValue function will not check for globally added claims. SuperTokens adds certain claims globally (for example the email verification claim in case you have enabled that recipe) which get checked only when running the <SessionAuth> wrapper is executed. Therefore, using useClaimValue is less favourable.