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.

Client Credentials Authentication

info

This page makes use of various OAuth2 terminology. If you need a refresher on what everything means, please check our separate page that explains most of the concepts.

Before going into the actual steps let's start by imagining a real life example that we can reference along the way. This will make it easier to understand what we are doing.

We are going to configure authentication for the following setup:

  • A Calendar Service that exposes these actions: event.view, event.create, event.update and event.delete
  • A File Service that exposes these actions: file.view, file.create, file.update and file.delete
  • A Task Service that interacts with the Calendar Service and the File Service in the process of scheduling a task

Our aim will be to allow the Task Service to perform an authenticated action on the Calendar Service. Now let's get into the actual steps.

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 microservices 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 attributes based on that
curl -X POST /recipe/oauth2/admin/clients \
-H "Content-Type: application/json" \
-H "api-key: " \
-d '{
"clientName": "<YOUR_CLIENT_NAME>",
"grantTypes": ["client_credentials"],
"scope": "<custom_scope_1> <custom_scope_2>",
"audience": ["<AUDIENCE_NAME>"]
}'
  • clientName - Then name of the client that will be used later for identification.
  • grantTypes - The grant types that the Client will use.
    • clientCredentials: Allows the client to directly request an Access Token by authenticating itself with the Authorization Server using its own client credentials.
  • audience - Value used to identify for whom a token was issued. The created client will be able to generate access token only for the specified audiences.
  • scope - A space separated string of scopes that the Client will request access to.
Custom Example

To create a client for our Task Service we will have to use the following attributes:

  {
"clientName": "Task Service",
"grantTypes": ["client_credentials"],
"scope": "event.view event.create event.edit event.delete file.view file.create file.edit file.delete",
"audience": ["event", "file"]
}

This will allow the Task Service to perform all types of actions against both of the other services as long as it has a valid OAuth2 Access Token.

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

{
"clientName": "<YOUR_CLIENT_NAME>",
"clientId": "<CLIENT_ID>",
"clientSecret": "<CLIENT_SECRET>",
"callbackUrls": [],
}

Change the default token lifespan#

By default, the token used in the authorization flow will have a 1 hour lifespan.

We recommend that you change it in order to use short lived tokens for improved security. To do this you will need to set the clientCredentialsGrantAccessTokenLifespan property 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").

3. Set Up your Authorization Service#

In your Authorization Service backend 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(),
]
});

4. Generate Access Tokens#

Now you can directly call the Authorization Server to generate Access Tokens. Check the following code snippet to see how you can do that:


curl -X POST <YOUR_API_DOMAIN>/auth/oauth/token \
-H "Content-Type: application/json" \
-d '{
"clientId": "<CLIENT_ID>",
"clientSecret": "<CLIENT_SECRET>",
"grantType": "client_credentials",
"scope": ["<RESOURCE_SCOPE>"],
"audience": "<AUDIENCE>"
}'

You should limit the scopes that you are requesting to just the ones necessary to perform the desired action.

Custom Example

If the Task Service wants to create an event on the Calendar Service we will have to generate a token with the following attributes:

  {
"clientId": "<TASK_SERVICE_CLIENT_ID>",
"clientSecret": "<TASK_SERVICE_CLIENT_SECRET>",
"grantType": "client_credentials",
"scope": ["event.create"],
"audience": "event"
}

The Authorization Server will return a response that will look like this:

{
"accessToken": "<TOKEN_VALUE>",
"expiresIn": 3600
}

You will have to save the accessToken in memory so that you can use it in the next step. The expiresIn field will tell you how long the token is valid for.

Each service that you communicate with will need its own token.

Now that you have an OAuth2 Access Token you can use it when communicating with the other services. Just keep in mind to generate a new one when it expires.

5. Verify an OAuth2 Access Token#

In order to check the validity of a token we recommend using a generic JWT verification library.

Besides the standard OAuth2 token claims our implementation includes an additional one called stt. This stands for SuperTokens Token Type. It is used to make sure that the validation is performed for the correct token type:

  • 0 represents a SuperTokens Session Access Token
  • 1 represents an OAuth2 Access Token
  • 2 represents an OAuth2 ID Token.

For NodeJS you can use jose to verify the token.

import jose from "jose";

const JWKS = jose.createRemoteJWKSet(new URL('<YOUR_API_DOMAIN>/auth/jwt/jwks.json'))

async function validateClientCredentialsToken(jwt: string) {
const requiredScope = "<YOUR_REQUIRED_SCOPE>";
const audience = '<AUDIENCE>';

try {
const { payload } = await jose.jwtVerify(jwt, JWKS, {
audience,
requiredClaims: ['stt', 'scp'],
});

if(payload.stt !== 1) return false;

const scopes = payload.scp as string[];
return scopes.includes(requiredScope);
} catch (err) {
return false;
}
}

Custom Example

If the Task Service uses the previously generated token to create a calendar event, the Calendar Service will have to check the following:

  • The stt claim should be set to 1
  • The scp claim contains event.create
  • The aud calim should be set to event

Handle Both SuperTokens Session Tokens and OAuth2 Access Tokens#

If you are using your Authorization Service also as a Resource Server you will have to account for this in the way you 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.

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

async function verifySession(req: Request, 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 that has been validated
if (session !== undefined) {
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 {
await validateToken(jwt);
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
// Use the previous example to extend it
async function validateToken(jwt: string) {
const { payload } = await jose.jwtVerify(jwt, JWKS, {
requiredClaims: ["stt", "scp", "sub"],
});

if (payload.stt !== 1) throw new Error("Invalid token");


// If the Authorizaton Server will handle different types of Authorization Flows
// You can differentiate between the different types of tokens by checking the `sessionHandle` claim
const sessionHandle = payload['sessionHandle'] as string | undefined;
if(sessionHandle === undefined) {
// We are dealing with a Client Credentials Token
// You can perform microservice authentication checks here
} else {
// Here we are validating tokens that have been generated in the Authorization Code Flow
}
}


// 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
});