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
andevent.delete
- A File Service that exposes these actions:
file.view
,file.create
,file.update
andfile.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 DashboardYou will first have to enable the OAuth2 features from the SuperTokens.com Dashboard.
- Open the SuperTokens.com Dashboard
- Click on the Enabled Paid Features button
- Click on Managed Service
- Check the OAuth 2.0 option
- Click Save
Now you should be able to use the OAuth2 recipes in your applications.
#
2. Create the OAuth2 Clients- Single app setup
- Multi app setup
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 lifespanBy 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 ServiceIn your Authorization Service backend you will need to initialize the OAuth2Provider recipe.
- NodeJS
- GoLang
- Python
Important
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(),
]
});
caution
At the moment we do not have support creating OAuth2 providers in the Go SDK. You can use the legacy method in order to authenticate microservices based on your language.
caution
At the moment we do not have support creating OAuth2 providers in the Python SDK. You can use the legacy method in order to authenticate microservices based on your language.
#
4. Generate Access TokensNow 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 TokenIn 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 Token1
represents an OAuth2 Access Token2
represents an OAuth2 ID Token.
- NodeJS
- GoLang
- Python
- Java
- PHP
- C#
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;
}
}
You can use the jwx library to verify the token.
import (
"context"
"fmt"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/jwk"
)
func ValidateToken(token string) bool {
apiDomain := "<YOUR_API_DOMAIN>"
apiBasePath := "/auth"
requiredScope := "<REQUIRED_SCOPE>"
jwksURL := fmt.Sprintf("%s%sjwt/jwks.json", apiDomain, apiBasePath)
jwks, err := jwk.Fetch(context.Background(), jwksURL)
if err != nil {
return false
}
parsedToken, err := jwt.Parse(
[]byte(token),
jwt.WithKeySet(jwks),
jwt.WithClaimValue("stt", 1),
jwt.WithAudience("<AUDIENCE>"),
)
if err != nil {
return false
}
scp, ok := parsedToken.Get("scp")
if !ok {
return false
}
scopes, ok := scp.([]interface{})
if !ok {
return false
}
for _, scope := range scopes {
if scope, ok := scope.(string); ok && scope == requiredScope {
return true
}
}
return false;
}
You can use the PyJWT library to verify the token.
from typing import Optional, List
import requests
import jwt
from jwt import PyJWKClient
def validate_token(token: str) -> bool:
api_domain = "<YOUR_API_DOMAIN>"
api_base_path = "/auth"
audience = "<AUDIENCE>"
required_scope = "<YOUR_REQUIRED_SCOPE>"
jwks_url = f"{api_domain}{api_base_path}jwt/jwks.json"
jwks_client = PyJWKClient(jwks_url)
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
audience=audience,
options={"require": ["stt", "scp"]}
)
stt: Optional[int] = decoded.get('stt')
if stt != 1:
return False
scopes: List[str] = decoded.get('scp', [])
if not isinstance(scopes, list) or required_scope not in scopes:
return False
return True
except Exception as e:
return False
You can use the Firebase JWT library to verify the token.
require 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
function validateToken($jwt) {
$apiDomain = "<YOUR_API_DOMAIN>";
$apiBasePath = "/auth";
$jwksUrl = $apiDomain . $apiBasePath . '/jwt/jwks.json';
$requiredScope = "<YOUR_REQUIRED_SCOPE>";
$audience = "<AUDIENCE>";
$jwks = json_decode(file_get_contents($jwksUrl), true);
try {
$decoded = JWT::decode($jwt, JWK::parseKeySet($jwks), 'RS256'));
if ($decoded->aud !== $audience) {
return false;
}
if ($decoded->sst !== 1) {
return false;
}
return in_array($requiredScope, $decoded->scp);
} catch (Exception $e) {
return false;
}
}
You can use the Auth0 JWT library to verify the token.
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.auth0.jwt.JWTVerifier.Base;
import com.auth0.jwt.algorithms.Algorithm;
import java.net.URL;
import java.util.Map;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class JWTVerifier {
private static final String JWKS_URL = "<YOUR_API_DOMAIN>/authjwt/jwks.json";
private static final String AUDIENCE = "<AUDIENCE>";
private static Map<String, Object> fetchJWKS() throws Exception {
URL url = new URL(JWKS_URL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
InputStream responseStream = connection.getInputStream();
Scanner scanner = new Scanner(responseStream, StandardCharsets.UTF_8.name());
String responseBody = scanner.useDelimiter("\\A").next();
scanner.close();
return JWT.decode(responseBody).getHeader();
}
public static boolean validateToken(String token) {
try {
Map<String, Object> jwks = fetchJWKS();
Algorithm algorithm = Algorithm.RSA256(jwks.get("x5c"), null);
JWTVerifier verifier = JWT.require(algorithm)
.withAudience(AUDIENCE)
.build();
DecodedJWT jwt = verifier.verify(token);
if(jwt.getClaim("sst").asInt() != 1) {
return false;
}
List<String> scopes = jwt.getClaim("scp").asList();
return scopes.contains(requiredScope);
} catch (Exception e) {
return false;
}
}
}
You can use the IdentityModel library to verify the token.
using System;
using System.Linq;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
class ClientCredentialsTokenValidator
{
static async Task<bool> ValidateToken(string jwtStr)
{
string apiDomain = "<YOUR_API_DOMAIN>";
string apiBasePath = "/auth";
string audience = "<AUDIENCE>";
string requiredScope = "<REQUIRED_SCOPE>";
HttpClient client = new HttpClient();
var response = await client.GetStringAsync($"{apiDomain}{apiBasePath}jwt/jwks.json");
var jwks = new JsonWebKeySet(response);
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidAudience = audience,
IssuerSigningKeys = jwks.Keys
};
try
{
SecurityToken validatedToken;
var principal = tokenHandler.ValidateToken(jwtStr, validationParameters, out validatedToken);
var claims = principal.Claims.ToDictionary(c => c.Type, c => c.Value);
if (!claims.ContainsKey("stt") || claims["stt"] != "1")
{
return false;
}
var scopes = claims["scp"].Split(" ");
if (!scopes.Contains(requiredScope))
{
return false;
}
return true;
}
catch (Exception)
{
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 to1
- The
scp
claim containsevent.create
- The
aud
calim should be set toevent
#
Handle Both SuperTokens Session Tokens and OAuth2 Access TokensIf 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
});
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 lifespanBy 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 ServiceIn your Authorization Service backend you will need to initialize the OAuth2Provider recipe.
- NodeJS
- GoLang
- Python
Important
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(),
]
});
caution
At the moment we do not have support creating OAuth2 providers in the Go SDK. You can use the legacy method in order to authenticate microservices based on your language.
caution
At the moment we do not have support creating OAuth2 providers in the Python SDK. You can use the legacy method in order to authenticate microservices based on your language.
#
4. Generate Access TokensNow 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 TokenIn 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 Token1
represents an OAuth2 Access Token2
represents an OAuth2 ID Token.
- NodeJS
- GoLang
- Python
- Java
- PHP
- C#
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;
}
}
You can use the jwx library to verify the token.
import (
"context"
"fmt"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/jwk"
)
func ValidateToken(token string) bool {
apiDomain := "<YOUR_API_DOMAIN>"
apiBasePath := "/auth"
requiredScope := "<REQUIRED_SCOPE>"
jwksURL := fmt.Sprintf("%s%sjwt/jwks.json", apiDomain, apiBasePath)
jwks, err := jwk.Fetch(context.Background(), jwksURL)
if err != nil {
return false
}
parsedToken, err := jwt.Parse(
[]byte(token),
jwt.WithKeySet(jwks),
jwt.WithClaimValue("stt", 1),
jwt.WithAudience("<AUDIENCE>"),
)
if err != nil {
return false
}
scp, ok := parsedToken.Get("scp")
if !ok {
return false
}
scopes, ok := scp.([]interface{})
if !ok {
return false
}
for _, scope := range scopes {
if scope, ok := scope.(string); ok && scope == requiredScope {
return true
}
}
return false;
}
You can use the PyJWT library to verify the token.
from typing import Optional, List
import requests
import jwt
from jwt import PyJWKClient
def validate_token(token: str) -> bool:
api_domain = "<YOUR_API_DOMAIN>"
api_base_path = "/auth"
audience = "<AUDIENCE>"
required_scope = "<YOUR_REQUIRED_SCOPE>"
jwks_url = f"{api_domain}{api_base_path}jwt/jwks.json"
jwks_client = PyJWKClient(jwks_url)
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
audience=audience,
options={"require": ["stt", "scp"]}
)
stt: Optional[int] = decoded.get('stt')
if stt != 1:
return False
scopes: List[str] = decoded.get('scp', [])
if not isinstance(scopes, list) or required_scope not in scopes:
return False
return True
except Exception as e:
return False
You can use the Firebase JWT library to verify the token.
require 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
function validateToken($jwt) {
$apiDomain = "<YOUR_API_DOMAIN>";
$apiBasePath = "/auth";
$jwksUrl = $apiDomain . $apiBasePath . '/jwt/jwks.json';
$requiredScope = "<YOUR_REQUIRED_SCOPE>";
$audience = "<AUDIENCE>";
$jwks = json_decode(file_get_contents($jwksUrl), true);
try {
$decoded = JWT::decode($jwt, JWK::parseKeySet($jwks), 'RS256'));
if ($decoded->aud !== $audience) {
return false;
}
if ($decoded->sst !== 1) {
return false;
}
return in_array($requiredScope, $decoded->scp);
} catch (Exception $e) {
return false;
}
}
You can use the Auth0 JWT library to verify the token.
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.auth0.jwt.JWTVerifier.Base;
import com.auth0.jwt.algorithms.Algorithm;
import java.net.URL;
import java.util.Map;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class JWTVerifier {
private static final String JWKS_URL = "<YOUR_API_DOMAIN>/authjwt/jwks.json";
private static final String AUDIENCE = "<AUDIENCE>";
private static Map<String, Object> fetchJWKS() throws Exception {
URL url = new URL(JWKS_URL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
InputStream responseStream = connection.getInputStream();
Scanner scanner = new Scanner(responseStream, StandardCharsets.UTF_8.name());
String responseBody = scanner.useDelimiter("\\A").next();
scanner.close();
return JWT.decode(responseBody).getHeader();
}
public static boolean validateToken(String token) {
try {
Map<String, Object> jwks = fetchJWKS();
Algorithm algorithm = Algorithm.RSA256(jwks.get("x5c"), null);
JWTVerifier verifier = JWT.require(algorithm)
.withAudience(AUDIENCE)
.build();
DecodedJWT jwt = verifier.verify(token);
if(jwt.getClaim("sst").asInt() != 1) {
return false;
}
List<String> scopes = jwt.getClaim("scp").asList();
return scopes.contains(requiredScope);
} catch (Exception e) {
return false;
}
}
}
You can use the IdentityModel library to verify the token.
using System;
using System.Linq;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
class ClientCredentialsTokenValidator
{
static async Task<bool> ValidateToken(string jwtStr)
{
string apiDomain = "<YOUR_API_DOMAIN>";
string apiBasePath = "/auth";
string audience = "<AUDIENCE>";
string requiredScope = "<REQUIRED_SCOPE>";
HttpClient client = new HttpClient();
var response = await client.GetStringAsync($"{apiDomain}{apiBasePath}jwt/jwks.json");
var jwks = new JsonWebKeySet(response);
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidAudience = audience,
IssuerSigningKeys = jwks.Keys
};
try
{
SecurityToken validatedToken;
var principal = tokenHandler.ValidateToken(jwtStr, validationParameters, out validatedToken);
var claims = principal.Claims.ToDictionary(c => c.Type, c => c.Value);
if (!claims.ContainsKey("stt") || claims["stt"] != "1")
{
return false;
}
var scopes = claims["scp"].Split(" ");
if (!scopes.Contains(requiredScope))
{
return false;
}
return true;
}
catch (Exception)
{
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 to1
- The
scp
claim containsevent.create
- The
aud
calim should be set toevent
#
Handle Both SuperTokens Session Tokens and OAuth2 Access TokensIf 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
});