Multiple frontend domains with a common backend
Overview
You can implement the following guide if you have multiple frontend applications
that use the same backend service
.
The authentication flow works in the following way:
frontend
application:- The application
frontend
redirects the user to the Authorization Service backend, using the authorize URL. - The Authorization Service backend redirects the user to the login UI.
- The Authorization Service backend redirects the user to the
callback URL
.
- The
frontend
uses the callback URL information to obtain a OAuth2 Access Token from the Authorization Service backend.
![Multiple Frontend Domains with a Single Backend](/img/oauth/multiple-frontend-domains-with-a-single-backend.png)
Before you start
This feature is only available to paid users.
These instructions assume that you already have gone through the main quickstart guide. If you have skipped that page, please follow the tutorial and return here once you're done.
If your frontend applications are on the same domain, but on different sub-domains, you can use Session Sharing Across Subdomains.
Steps
1. Enable the Unified Login feature
Go to the SuperTokens.com SaaS Dashboard and follow these instructions:
- Click on the Enabled Paid Features button
- Click on Managed Service
- Check the Unified Login / M2M option
- Click Save
2. Create the OAuth2 Clients
For each of your frontend
applications create a separate OAuth2 client.
This can occur by directly calling the SuperTokens Core API.
curl --location --request POST '<CORE_API_ENDPOINT>/recipe/oauth/clients' \
--header 'api-key: <YOUR_API_KEY>' \
--header 'Content-Type: application/json; charset=utf-8' \
--data '
{
"clientName": "<YOUR_CLIENT_NAME>",
"responseTypes": ["code"],
"grantTypes": ["authorization_code", "refresh_token"],
"scope": "offline_access <custom_scope_1> <custom_scope_2>",
"redirectUris": ["https://<YOUR_APPLICATION_DOMAIN>/oauth/callback"],
}
'
You have to save the create OAuth2 Client response because this is not persisted internally for security reasons. The information is necessary in the next steps.
3. Set up the Authorization Service Backend
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.3.1 Initialize the OAuth2 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(),
]
});
3.2 Update the CORS configuration
Set up the Backend API to allow 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,
}));
3.3 Implement a custom session verification function
Given that the backend, the Authorization Server, also acts as a Resource Server you have to account for this in the session verification process.
This is necessary because the flow uses two types of tokens:
- SuperTokens Session Access Token: Used during the login and logout.
- OAuth2 Access Token: Used to access protected resources and perform actions that need authorization.
Hence the logic should 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 the separate guide.
4. Configure the Authorization Service Frontend
What type of UI are you using?
4.1 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?
import React from 'react';
import {
BrowserRouter,
Routes,
Route,
Link
} from "react-router-dom";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui";
import * as reactRouterDom from "react-router-dom";
class App extends React.Component {
render() {
return (
<SuperTokensWrapper>
<BrowserRouter>
<Routes>
{/*This renders the login UI on the /auth route*/}
{getSuperTokensRoutesForReactRouterDom(reactRouterDom, [OAuth2ProviderPreBuiltUI])}
{/*Your app routes*/}
</Routes>
</BrowserRouter>
</SuperTokensWrapper>
);
}
}
4.2 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 to keep track of the authentication state.
- OAuth2 Access Token: Returned after a successful login attempt. It can then access protected resources.
By default, the SuperTokens frontend SDK intercepts all the network requests sent to your Backend API and adjusts them based on the SuperTokens Session Tokens. This allows operations, such as automatic token refreshing or adding authorization headers, without needing to configure anything else.
Given that in the scenario you are implementing, the OAuth2 Access Tokens serve authorization purposes.
The automatic request interception causes conflicts.
To prevent this, you need to override the shouldDoInterceptionBasedOnUrl
function in the Session.init
call.
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 ensures that calls made from the Frontend SDKs continue to use the SuperTokens Session Tokens. As a result, the authentication flow ends up working.
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
You can use the react-oidc-context library. Follow the instructions from the library's page.
Identify the configuration parameters based on the response received on step 2, when creating the OAuth2 Client.
authority
corresponds to the endpoint of the Authorization Service<YOUR_API_DOMAIN>/auth
clientID
corresponds toclientId
redirectUri
corresponds to a value fromcallbackUrls
scope
corresponds toscope
If you are using a multi-tenant setup, you also need to specify the tenantId
parameter in the authorization URL.
To do this, set the extraQueryParams
property with a specific value that should look like this: { tenant_id: "<TENANT_ID>" }
.
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 test your login flow. Use the setup created in the previous step to check if the authentication flow completes without any issues.