Skip to main content
Paid Feature

This is a paid feature.

You can click on the Enable Paid Features button on our dashboard, and follow the steps from there on. Once enabled, this feature is free on the provided development environment.

Multiple Frontend Domains with a Common Backend

You can use the following guide if you have multiple frontend applications that use the same backend service.

Using the actual OAuth 2.0 terminology, we can say that each frontend can be considered a Client and the backend will act as both an Authorization Server and a Resource Server.

info

If your frontend applications are on the same domain, but on different sub-domains, you can use Session Sharing Across Subdomains

Multiple Frontend Domains with a Single Backend

The authentication flow will work in the following way:

  1. The User accesses the frontend app
  2. The User completes the login attempt
  3. The User accesses the callback URL
    • The frontend uses the callback URL information to obtain a OAuth2 Access Token from the Authorization Service.
info

This guide assumes that you already have setup and configured SuperTokens in your Authorization Service.

For more information on how to do that please check our quickstart guide.

1. Enable the OAuth2 features from the Dashboard#

You will first have to enable the OAuth2 features from the SuperTokens.com Dashboard.

  1. Open the SuperTokens.com Dashboard
  2. Click on the Enabled Paid Features button
  3. Click on Managed Service
  4. Check the OAuth 2.0 option
  5. Click Save

Now you should be able to use the OAuth2 recipes in your applications.

2. Create the OAuth2 Clients#

For each of your frontend applications you will have to create a separate OAuth2 client. This can be done by directly calling the SuperTokens Core API.

# You will have to run this for each one of your applications
# Adjust the clientName and redirectUri based on that
curl -X POST /recipe/oauth/clients \
-H "Content-Type: application/json" \
-H "api-key: " \
-d '{
"clientName": "<YOUR_CLIENT_NAME>",
"responseTypes": ["code"],
"grantTypes": ["authorization_code", "refresh_token"],
"tokenEndpointAuthMethod": "none",
"scope": "offline_access <custom_scope_1> <custom_scope_2>",
"redirectUri": ["https://<YOUR_APPLICATION_DOMAIN>/oauth/callback"],
}'
  • clientName - A human-readable name of the client that will be used for identification.
  • responseTypes - Specifies the types of responses your client expects from the Authorization Server.
  • grantTypes - The grant types that the Client will use.
  • tokenEndpointAuthMethod - Indicates that the process of obtaining an OAuth2 Access Token will not make use of the client secret
  • redirectUri - A list of URIs to which the Authorization Server will send the user-agent (browser) after completing the authorization step. These can be deep links to mobile or desktop apps as well, but they must be exact URLs, without wildcards.
  • scope - A space separated string of scopes that the Client will request access to.

If the creation was successful, the API will return a response that looks like this:


{
"clientName": "<YOUR_CLIENT_NAME>",
"clientId": "<CLIENT_ID>",
"callbackUrls": ["https://<YOUR_APPLICATION_DOMAIN>/oauth/callback"],
}
caution

You will have to save this response because we do not persist it internally for security reasons. In the next steps we will use the values to complete several configurations.

Change the default token lifespan#

By default, the tokens used in the authorization flow will have the following lifespans:

If you want to change the default values you need to specify additional properties in the Client creation request body. Use string values that signify time duration in milliecoseconds, seconds, minutes or hours (e.g. "2000ms", "60s", "30m", "1h"). There are no limits on the duration of each token.

  • OAuth2 Access Token - Set the authorizationCodeGrantAccessTokenLifespan property.
  • OAuth2 ID Token - Set the authorizationCodeGrantIdTokenLifespan property.
  • OAuth2 Refresh Token - Set both the authorizationCodeGrantRefreshTokenLifespan and the refreshTokenGrantRefreshTokenLifespan properties to the same value.

Disable Refresh Token Rotation#

By default, a refresh token is one time use. This means, once a refresh token is used to get a new access token, it cannot be used again. If your use case cannot accomodate the process of changing the OAuth2 Refresh Token for a new one, you can make it so that this behavior does not apply for your implementation.

In order to achieve this behavior just set the enableRefreshTokenRotation property to false in the Client creation request body.

3. Set Up your Authorization Service Backend#

Initialize the OAuth2 Recipes#

In your Authorization Service you will need to initialize the OAuth2Provider recipe.

Update the supertokens.init call to include the new recipe.


import supertokens from "supertokens-node";
import OAuth2Provider from "supertokens-node/recipe/oauth2provider";

supertokens.init({
supertokens: {
connectionURI: "...",
apiKey: "...",
},
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
recipeList: [
OAuth2Provider.init(),
]
});

Update the CORS configuration#

You need to setup the Backend API so that it allows requests from all the frontend domains.

import express from "express";
import cors from "cors";
import supertokens from "supertokens-node";
import { middleware } from "supertokens-node/framework/express";

const app = express();

// Add your actual frontend domains here
const allowedOrigins = ["<YOUR_WEBSITE_DOMAIN>", "<CLIENT_DOMAIN_1>", "<CLIENT_DOMAIN_2>"];

app.use(cors({
origin: allowedOrigins,
allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()],
credentials: true,
}));

Implement a Custom Session Verification Function#

Given that the backend that represents the Authorization Server will also act as a Resource Server we will have to account for this in the way we verify the sessions.

This is needed because we are using two types of tokens:

  • SuperTokens Session Access Token: Used during the login/logout flows.
  • OAuth2 Access Token: Used to access protected resources and perform actions that need authorization.

Hence we need a way to distinguish between these two and prevent errors.

Here is an example of how to implement this in the context of an Express API:

import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import express, { Request, Response, NextFunction } from 'express';
import jose from "jose";

interface RequestWithUserId extends Request {
userId?: string;
}

async function verifySession(req: RequestWithUserId, res: Response, next: NextFunction) {
let session = undefined;
try {
session = await Session.getSession(req, res, { sessionRequired: false });
} catch (err) {
if (
!Session.Error.isErrorFromSuperTokens(err) ||
err.type !== Session.Error.TRY_REFRESH_TOKEN
) {
return next(err);
}
}

// In this case we are dealing with a SuperTokens Session
if (session !== undefined) {
const userId = session.getUserId();
req.userId = userId;
return next();
}

// The OAuth2 Access Token needs to be manually extracted and validated
let jwt: string | undefined = undefined;
if (req.headers["authorization"]) {
jwt = req.headers["authorization"].split("Bearer ")[1];
}
if (jwt === undefined) {
return next(new Error("No JWT found in the request"));
}

try {
const tokenPayload = await validateToken(jwt, '<CUSTOM_SCOPE>');
const userId = tokenPayload.sub;
req.userId = userId;
return next();
} catch (err) {
return next(err);
}
}

const JWKS = jose.createRemoteJWKSet(
new URL("<YOUR_API_DOMAIN>/authjwt/jwks.json"),
);

// This is a basic example on how to validate an OAuth2 Token
// We have a separate page that talks more in depth about the process
async function validateToken(jwt: string, requiredScope: string) {
const { payload } = await jose.jwtVerify(jwt, JWKS, {
requiredClaims: ["stt", "scp", "sub"],
});

if (payload.stt !== 1) throw new Error("Invalid token");
const scopes = payload.scp as string[];
if (!scopes.includes(requiredScope)) throw new Error("Invalid token");

return payload;
}


// You can then use the function as a middleware for a protected route
const app = express();
app.get("/protected", verifySession, async (req, res) => {
// Custom logic
});

For more information on how to verify the OAuth2 Access Tokens please check our separate guide.

4. Configure the Authorization Service Frontend#

Initialize the OAuth2Provider recipe on the frontend of your Authorization Service.

info

If you want to use your own custom UI check our separate guide that explains all the steps that you have to take into account.

Initialize the Recipe#

Add the import statement for the new recipe and update the list of recipe to also include the new initialization.

import OAuth2Provider from "supertokens-auth-react/recipe/oauth2provider";
import SuperTokens from "supertokens-auth-react";

SuperTokens.init({
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
recipeList: [
OAuth2Provider.init()
]
});

Include the pre built UI in the rendering tree.#

Do you use react-router-dom?
YesNo

Disable Network Interceptors#

The Authorization Service Frontend that you are configuring makes use of two types of access tokens:

  • SuperTokens Session Access Token: Used only during the login flow in order to keep track of the authentication state.
  • OAuth2 Access Token: Returned after a successfull login attempt. It can be used afterwards to access protected resources.

By default, the SuperTokens frontend SDK will intercept all the network requests that you send to your Backend API and adjust them, based on the SuperTokens Session Tokens. This allows us to perform operations, such as automatic token refreshing or adding authorization headers, without you having to configure anything else.

Given that in the scenario that you are implementing the OAuth2 Access Tokens are used for authorization. The automatic request interception will end up causing conflicts. In order to prevent this you will have to override the shouldDoInterceptionBasedOnUrl function in the Session.init call.

caution

The code samples assume that you are using /auth as the apiBasePath for the backend authentication routes. If that is different please adjust them based on your use case.

import Session from "supertokens-auth-react/recipe/session";

Session.init({
override: {
functions: (oI) => {
return {
...oI,
shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => {
try {
let urlObj = new URL(url);
// Interception should be done only for routes that need the SuperTokens Session Tokens
const isAuthApiRoute = urlObj.pathname.startsWith("/auth");
const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth");
if (!isAuthApiRoute || isOAuth2ApiRoute) {
return false;
}
} catch (ignored) { }
return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain);
}
}
}
}
})

The code snippet only allows interception for API endpoints that start with /auth. This will ensure that calls made from our Frontend SDKs will continue to use the SuperTokens Session Tokens. As a result, the authentication flow ends up working properly.

For the other routes, you have full control on how you want to attach the OAuth2 Access Tokens to the API calls.

5. Update the login flow in your frontend applications#

Use a generic OAuth2 library to handle the login flow

We recommend using the react-oidc-context library. Just follow the instructions from the library's page.

The configuration parameters can be determined based on the response that we received on step 2, when we created the OAuth2 Client.

  • authority corresponds to the endpoint of the Authorization Service <YOUR_API_DOMAIN>/auth
  • clientID corresponds to clientId
  • redirectUri corresponds to a value from callbackUrls
  • scope corresponds to scope

If you are using a multi-tenant setup, you will also need to specify the tenantId parameter in the authorization URL. To do this just set the extraQueryParams property with a specific value that should look like this: { tenant_id: "<TENANT_ID>" }.

info

If you want to use the OAuth2 Refresh Tokens make sure to include the offline_access scope during the initialization step.

6. Test the new authentication flow#

With everything set up, you can now test your login flow. Just use the setup that you have created in the previous step to check if the authentication flow completes without any issues.