Legacy Flow
This custom flow makes use of the SuperTokens Core without adhering to the OAuth2 standard. It involves creating private access tokens and passing them to the microservices.
The authentication sequene will work in the following way:
- Microservice
M1
requests a JWT (JSON Web Token) from the SuperTokens Core - Microservice
M1
sends the JWT toM2
- Microservice
M2
will verifies the JWT and completes the action if the JWT is valid
#
Creating a JWT#
When to create a JWT?The first step is to create a JWT from the microservice that will be sending the request (let's call this microservice M1
). This JWT will be verified by other microservices when M1
sends them a request. Since this JWT remains static per microservice, the best time to create this is on process starts - i.e. when M1
starts.
#
What to add in the JWT?The JWT can contain any information you like, but at a minimum, it needs to contain information proving that it is a microservice that is allowed to query other microservices in your infrastructure. This is needed since you may issue a JWT to an end user as well, and they should not be able to query any microservice directly.
We can add the following claim in the JWT to "mark" the JWT as one that is meant for microservice auth only:
{..., "source": "microservice", ...}
In the receiving microservice (M2
), we can then verify the JWT and check that this claim is present before serving the request.
#
Code for creating a JWT- Using SuperTokens SDK
- Without SDK
First, we need to initialise the JWT
recipe in the supertokens.init
function call:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import supertokens from "supertokens-node"
import jwt from "supertokens-node/recipe/jwt"
supertokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
supertokens: {
connectionURI: "...", // location of the core
apiKey: "..." // provide the core's API key if configured
},
recipeList: [
jwt.init()
]
})
import (
"github.com/supertokens/supertokens-golang/recipe/jwt"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
AppInfo: supertokens.AppInfo{
AppName: "...",
WebsiteDomain: "...",
APIDomain: "...",
},
Supertokens: &supertokens.ConnectionInfo{
ConnectionURI: "...", // location of the core
APIKey: "...", // provide the core's API key if configured
},
RecipeList: []supertokens.Recipe{
jwt.Init(nil),
},
})
}
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import jwt
init(
app_info=InputAppInfo(
app_name="...",
api_domain="...",
website_domain="...",
),
supertokens_config=SupertokensConfig(
connection_uri="...", # location of the core
api_key="..." # provide the core's API key if configured
),
framework='django',
recipe_list=[
jwt.init(),
],
)
important
- The value of
apiDomain
should be the domain part of the JWKS URL that will be used to verify the JWT (fromM2
). This should ideally be the domain of the microservice that has all the other SuperTokens' recipes initialised in them. - If this microservice does not initialise any other recipe, the values of
appName
andwebsiteDomain
don't matter.
After this, you can use the JWT recipe to create your own JWT whenever required:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import jwt from "supertokens-node/recipe/jwt"
async function createJWT(payload: any) {
let jwtResponse = await jwt.createJWT({
...payload,
source: "microservice"
});
if (jwtResponse.status === "OK") {
// Send JWT as Authorization header to M2
return jwtResponse.jwt;
}
throw new Error("Unable to create JWT. Should never come here.")
}
import (
"fmt"
"github.com/supertokens/supertokens-golang/recipe/jwt"
)
func main() {
jwtResponse, err := jwt.CreateJWT(map[string]interface{}{
"source": "microservice",
// ...additional payload
}, nil, nil)
if err != nil {
// handle error
}
jwtString := jwtResponse.OK.Jwt
fmt.Println(jwtString)
// Send JWT as Authorization header to M2
}
- Asyncio
- Syncio
from supertokens_python.recipe.jwt import asyncio
from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult
async def create_jwt():
jwtResponse = await asyncio.create_jwt({
"source": "microservice",
# ... extra payload
})
if isinstance(jwtResponse, CreateJwtOkResult):
_ = jwtResponse.jwt
# Send JWT as Authorization header to M2
else:
raise Exception("Unable to create JWT. Should never come here.")
from supertokens_python.recipe.jwt.syncio import create_jwt
from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult
jwtResponse = create_jwt({
"source": "microservice",
# ... extra payload
})
if isinstance(jwtResponse, CreateJwtOkResult):
jwtStr = jwtResponse.jwt
# Send JWT as Authorization header to M2
else:
raise Exception("Unable to create JWT. Should never come here.")
note
By default, the lifetime of the JWT will be a 100 years. You can pass a second argument to the createJWT
function indicating a custom lifetime (in seconds) for the JWT.
note
By default, the JWT is signed by a static key, not subject to the key rotation normally applied to access tokens. You can pass false
as the third argument to the createJWT
function to use use the dynamic keys.
You can send a HTTP
request to the core as follows:
curl --location --request POST '${connectionURI}/recipe/jwt' \
--header 'rid: jwt' \
--header 'api-key: ${APIKey}' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"payload": {
"source": "microservice",
...
},
"useStaticSigningKey": true,
"algorithm": "RS256",
"jwksDomain": "${apiDomain}",
"validity": ${validityInSeconds}
}'
- The value of
${connectionURI}
is the core's location ${APIKey}
is the API key to query the core. This is only needed if an API key is configured on the core.- The value of
${apiDomain}
is the domain on which the JWKs URLs will be served from ${validityInSeconds}
is the lifetime of the JWT
An example response is as follows:
{
"status": "OK",
"jwt": "eyJraWQiOiI0YTE...rCFPcIRgzu_bChIIpFdA"
}
#
What to do with the JWT?Once the JWT is created, you can store it in a (globally accessible) variable and access it when you want to talk to a microservice. You can add the JWT as an Authorization: Bearer
token like so:
curl --location --request POST 'https://microservice_location/path' \
--header 'Authorization: Bearer eyJraWQiOiI0YTE...rCFPcIRgzu_bChIIpFdA' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"request": "payload"
}'
#
Verify JWTsWhen a target microservice receives a JWT, it must first verify it before proceeding to serve the request. There are two steps here:
- A standard verification of the JWT
- Checking the JWT claim to make sure that another microservice has queried it.
#
Standard verification of a JWT#
Method 1) Using JWKS endpoint- NodeJS
- GoLang
- Python
Important
#
a) Get JWKS endpointThe JWKS endpoint is {apiDomain}/{apiBasePath}/jwt/jwks.json
. Here the apiDomain
and apiBasePath
are values pointing to the server in which you have initalised SuperTokens using our backend SDK.
#
b) Verify the JWTSome libraries let you provide a JWKS endpoint to verify a JWT. For example for NodeJS you can use jsonwebtoken
and jwks-rsa
together to achieve this.
import JsonWebToken, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
var client = jwksClient({
jwksUri: '<YOUR_API_DOMAIN>/auth/jwt/jwks.json'
});
function getKey(header: JwtHeader, callback: SigningKeyCallback) {
client.getSigningKey(header.kid, function (err, key) {
var signingKey = key!.getPublicKey();
callback(err, signingKey);
});
}
let jwt = "...";
JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) {
let decodedJWT = decoded;
// Use JWT
});
Refer to this Github gist for a code reference of how use PyJWK
to do JWT verification. The gist contains two files:
jwt_verification.py
(which you can just copy / paste into your application). You will have to modify theJWKS_URI
in this file to point to your supertokens core instance (replacing thetry.supertokens.com
part of the URL). This file is written forsync
python apps, and can be modified to work withasync
apps as well.- This file essentially exposes a function called
verify_jwt
which takes an input JWT string. - This function takes care of caching public keys in memory + auto refetching if the public keys have changed (which happens automatically every 24 hours with supertokens). This will not cause any user logouts, and is just a security feature.
- This file essentially exposes a function called
views.py
: This is an exampleGET
API which extracts the JWT token from the authorization header in the request and calls theverify_jwt
function from the other file.
Refer to this Github gist for a code reference of how use the Golang jwt
lib to do session verification. The gist contains two files:
verifyToken.go
(which you can just copy / paste into your application). You will have to modify thecoreUrl
in this file to point to your supertokens core instance (replacing thetry.supertokens.com
part of the URL).- This file essentially exposes a function called
GetJWKS
which returns a reference to the JWKS public keys that can be used for JWT verification. - This function takes care of caching public keys in memory + auto refetching if the public keys have changed (which happens automatically every 24 hours with supertokens). This will not cause any user logouts, and is just a security feature.
- This file essentially exposes a function called
main.go
: This is an example of how to verify a JWT using the golang JWT verification lib along with our helper function to get the JWKs keys.
#
Method 2) Using public key string- NodeJS
- GoLang
- Python
Important
Some JWT verification libraries require you to provide the JWT secret / public key for verification. You can obtain the JWT secret from SuperTokens in the following way:
First, we query the
JWKS.json
endpoint:curl --location --request GET '<YOUR_API_DOMAIN>/auth/jwt/jwks.json'
{
"keys": [
{
"kty": "RSA",
"kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86",
"n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=",
"e": "AQAB",
"alg": "RS256",
"use": "sig"
},
{
"kty": "RSA",
"kid": "d-230...802340",
"n": "AMZruthvYz7...lx0rU8c=",
"e": "...",
"alg": "RS256",
"use": "sig"
}
]
}important
The above shows an example output which returns two keys. There could be more keys returned based on the configured key rotation setting in the core. If you notice, each key's
kid
starts with as-..
or ad-..
. Thes-..
key is a static key that will never change, whereasd-...
keys are dynamic keys that keep changing. So if you are hardcoding public keys somewhere, you always want to pick thes-..
key.Next, we run the NodeJS script below to convert the above output to a
PEM
file format.import jwkToPem from 'jwk-to-pem';
// This JWK is copied from the result of the above SuperTokens core request
let jwk = {
"kty": "RSA",
"kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86",
"n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=",
"e": "AQAB",
"alg": "RS256",
"use": "sig"
};
let certString = jwkToPem(jwk);The above snippet would generate the following certificate string:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxmu62G9jPsW34OnQEL9I
QQlpYr3Wz9gD5FHzWIsnoFNPqAmnQJxXgN8HKcVT/n11EY5nJVCkBboOudz/ovJm
... (truncated for display)
XhfWeIqBjrJheltfqKzgcJ+Fh91ptwTnbFgw2X6DD1cOOwjF8ExQO84VEdaXHStT
xwIDAQAB
-----END PUBLIC KEY-----Now you can use the generated PEM string in your code like shown below:
import JsonWebToken from 'jsonwebtoken';
// Truncated for display
let certificate = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki...\n-----END PUBLIC KEY-----";
let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header
JsonWebToken.verify(jwt, certificate, function (err, decoded) {
let decodedJWT = decoded;
// Use JWT
});
caution
Not applicable. Please use method 1 instead.
caution
Not applicable. Please use method 1 instead.
#
Claim verificationThe second step is to get the JWT payload and check that it has the "source": "microservice"
claim:
- NodeJS
- GoLang
- Python
Important
import JsonWebToken, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
var client = jwksClient({
jwksUri: '<YOUR_API_DOMAIN>/auth/jwt/jwks.json'
});
function getKey(header: JwtHeader, callback: SigningKeyCallback) {
client.getSigningKey(header.kid, function (err, key) {
var signingKey = key!.getPublicKey();
callback(err, signingKey);
});
}
let jwt = "...";
JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) {
let decodedJWT = decoded;
if (decodedJWT === undefined || typeof decodedJWT === "string" || decodedJWT.source === undefined || decodedJWT.source !== "microservice") {
// return a 401 unauthorised error
} else {
// handle API request...
}
});
Referring once again to this Github gist, we can see in views.py
, between lines 20 and 28, that we are checking for the value of a certain claim in the decoded JWT payload. You can do something similar and check for the source
claim whose value must be microservice
. If it is, then all's good, else you can return a 401.
Referring once again to this Github gist, we can see in main.go
, between lines 32 and 44, that we are checking for the value of a certain claim in the decoded JWT payload. You can do something similar and check for the source
claim whose value must be microservice
. If it is, then all's good, else you can return a 401.
#
M2M and frontend session verification for the same APIYou may have a setup wherein the same API is called from the frontend as well as from other microservices. The frontend session works differently than m2m sessions, so we have to account for both forms of token inputs.
Our approach here would be to first attempt frontend session verification, and if that fails, then attempt m2m jwt verification (using the above method). If both fails, then we send back a 401
response.
We will use the getSession
function for frontend session verification.
- NodeJS
- GoLang
- Python
Important
import express from "express";
import Session from "supertokens-node/recipe/session";
import JsonWebToken, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
let app = express();
var client = jwksClient({
jwksUri: '<YOUR_API_DOMAIN>/auth/jwt/jwks.json'
});
function getKey(header: JwtHeader, callback: SigningKeyCallback) {
client.getSigningKey(header.kid, function (err, key) {
var signingKey = key!.getPublicKey();
callback(err, signingKey);
});
}
app.post("/like-comment", async (req, res, next) => {
try {
let session = await Session.getSession(req, res, { sessionRequired: false })
if (session !== undefined) {
// API call from the frontend and session verification is successful..
let userId = session.getUserId();
} else {
// maybe this API is called from a microservice, so we attempt JWT verification as
// shown above.
let jwt = req.headers["authorization"];
jwt = jwt === undefined ? undefined : jwt.split('Bearer ')[1];
if (jwt === undefined) {
// return a 401 unauthorised error...
} else {
JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) {
let decodedJWT = decoded;
// microservices auth is successful..
});
}
}
} catch (err) {
next(err);
}
});
- Notice that we add the
sessionRequired: false
option when callinggetSession
. This is because in case the input tokens are from another microservice, then instead of throwing an unauthorised error, thegetSession
function will returnundefined
. It's important to note that if the session does exist, but the access token has expired, thegetSession
function will throw a try refresh token error, sending a 401 to the frontend. This will trigger a session refresh flow as expected. - If the
getSession
function returnsundefined
, it means that the session is not from the frontend and we can attempt a microservice auth verification using the JWT verification method shown previously in this page. - If that fails too, we can send back a
401
response.
from supertokens_python.recipe.session.asyncio import get_session
from django.http import HttpRequest
from typing import Union
async def like_comment(request: HttpRequest):
session = await get_session(request, session_required=False)
user_id = ""
if session is not None:
user_id = session.get_user_id()
else:
jwt: Union[str, None] = request.headers.get("Authorization")
if jwt is None:
# return a 401 unauthorised error...
pass
else:
jwt = jwt.split("Bearer ")[1]
# JWT verification (see previous step)
pass
print(user_id) # TODO
- Notice that we add the
sessionRequired: false
option when callinggetSession
. This is because in case the input tokens are from another microservice, then instead of throwing an unauthorised error, thegetSession
function will returnNone
. It's important to note that if the session does exist, but the access token has expired, thegetSession
function will throw a try refresh token error, sending a 401 to the frontend. This will trigger a session refresh flow as expected. - If the
getSession
function returnsNone
, it means that the session is not from the frontend and we can attempt a microservice auth verification using the JWT verification method shown previously in this page. - If that fails too, we can send back a
401
response.
import (
"fmt"
"net/http"
"strings"
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func likeCommentAPI(w http.ResponseWriter, r *http.Request) {
sessionRequired := false
sessionContainer, err := session.GetSession(r, w, &sessmodels.VerifySessionOptions{
SessionRequired: &sessionRequired,
})
if err != nil {
err = supertokens.ErrorHandler(err, r, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
return
}
if sessionContainer != nil {
userID := sessionContainer.GetUserID()
// TODO: API logic...
fmt.Println(userID)
} else {
// Check for JWT in the Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
// Return 401 Unauthorized error
w.WriteHeader(http.StatusUnauthorized)
return
}
// Split the Authorization header to get the JWT
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
// Return 401 Unauthorized error
w.WriteHeader(http.StatusUnauthorized)
return
}
jwt := parts[1]
fmt.Println(jwt)
// verify JWT based on code snippet here: https://gist.github.com/rishabhpoddar/8c26ed237add1a5b86481e72032abf8d
}
}
- Notice that we add the
sessionRequired: false
option when callinggetSession
. This is because in case the input tokens are from another microservice, then instead of throwing an unauthorised error, thegetSession
function will returnnil
. It's important to note that if the session does exist, but the access token has expired, thegetSession
function will return an error, and thesupertokens.ErrorHandler
will send a 401 to the frontend. This will trigger a session refresh flow as expected. - If the
getSession
function returnsnil
, it means that the session is not from the frontend and we can attempt a microservice auth verification using the JWT verification method shown previously in this page. - If that fails too, we can send back a
401
response.