Manually verify the JWT
There are three steps in doing session verification using JWTs:
- Verify the JWT signature and expiry using a JWT verification library
- Check for custom claim values for authorization.
- Preventing CSRF attacks in case you are using cookies to store the JWT.
#
Verifying a JWT using a jwt verification library#
Method 1) Using JWKS endpoint (recommended)- NodeJS
- GoLang
- Python
Important
Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is available at the following URL:
curl --location --request GET '<YOUR_API_DOMAIN>/auth/jwt/jwks.json'
Below is an example for NodeJS showing how you can use jsonwebtoken
and jwks-rsa
together to achieve JWT verification using the jwks.json
endpoint.
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 = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header
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 session 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. If you are using cookie based auth instead of header based auth, you should read the JWT from thesAccessToken
cookie in the request.
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. If you are using header based auth, you can fetch the JWT from theAuthorization Bearer
header, otherwise for cookie based auth, you can fetch it from thesAccessToken
cookie.
#
Method 2) Using public key string- NodeJS
- GoLang
- Python
Important
caution
This method is less secure compared to Method 1 because it disables key rotation of the access token signing key. In this case, if the private key is stolen somehow, it can be used indinitely to forge access tokens (Unless you manually change the key in the database).
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.One exception is that if you see a key with
kid
that doesn't start withs-
or withd-
, then you should treat that as a static key (This will only happen if you used to run an older SuperTokens core that was lesser than version5.0
).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
});The final step is to tell SuperTokens to always only use the static key when creating a new session. This can be done by setting the below config in the backend SDK:
import SuperTokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
SuperTokens.init({
supertokens: {
connectionURI: "...",
},
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
recipeList: [
Session.init({
useDynamicAccessTokenSigningKey: false,
})
]
});
caution
Updating this value will cause a spike in the session refresh API, as and when users visit your application.
caution
Not applicable. Please use method 1 instead.
caution
Not applicable. Please use method 1 instead.
#
Check for custom claim values for authorization- NodeJS
- GoLang
- Python
Important
Once you have verified the access token, you can fetch the payload and do authorization checks based on the values of the custom claims. For examlpe, if you want to do check for if the user's email is verified, you should check the st-ev
claim in the payload as shown below:
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 = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header
JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) {
if (err) {
// send a 401 to the frontend..
}
if (decoded !== undefined && typeof decoded !== "string") {
let isEmailVerified = (decoded as any)["st-ev"].v
if (!isEmailVerified) {
// send a 403 to the frontend..
}
}
});
Claims like email verification and user roles claims are added to the access token by our backend SDK automatically. You can even add your own custom claims to the access token payload and those claims will be in the JWT as expected.
important
On claim validation failure, you must send a 403
to the frontend which will cause our frontend SDK (pre built UI SDK) to recheck the claims added on the frontend and navigate to the right screen.
Referring once again to this Github gist, we can see in views.py
, between lines 20 and 28, that we are checking for the st-ev
claim in the JWT payload. If the claim is not present or is set to false
, we send a 403
to the frontend which will cause our frontend SDK (pre built UI SDK) to recheck the claims added on the frontend and navigate to the right screen.
Referring once again to this Github gist, we can see in main.go
, between lines 32 and 44, that we are checking for the st-ev
claim in the JWT payload. If the claim is not present or is set to false
, we send a 403
to the frontend which will cause our frontend SDK (pre built UI SDK) to recheck the claims added on the frontend and navigate to the right screen.
#
Check for anti-csrf during authorizationimportant
You will need to check for anti-csrf for NON GET requests when cookie based authentication is enabled.
There are two methods for configuring CSRF protection:
VIA_CUSTOM_HEADER
VIA_TOKEN
VIA_CUSTOM_HEADER
is set#
Checking for anti-csrf when VIA_CUSTOM_HEADER
is automatically set if sameSite
is none
or if your apiDomain
and websiteDomain
do not share the same top level domain. In this case you will need to check for the presence of the rid
header from incoming requests.
VIA_TOKEN
is set#
Checking for anti-csrf when When configured with VIA_TOKEN
, an explicit anti-csrf
token will be attached as a header to requests with anti-csrf
as the key. To verify the anti-csrf
token you will need to compare it the to value of the antiCsrfToken
key from the payload of the decoded JWT.