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)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
});
#
Method 2) Using public key stringcaution
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:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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.
import (
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
useDynamicAccessTokenSigningKey := false
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
session.Init(&sessmodels.TypeInput{
UseDynamicAccessTokenSigningKey: &useDynamicAccessTokenSigningKey,
}),
},
})
}
caution
Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop):
If useDynamicAccessTokenSigningKey
is false:
UPDATE session_info SET use_static_key = true;
Else if useDynamicAccessTokenSigningKey
is true:
UPDATE session_info SET use_static_key = false;
If you are using the managed core, you can send an email to us about this, and we will run the query for you.
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import session
init(
app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
framework='...',
recipe_list=[
session.init(
use_dynamic_access_token_signing_key=False
)
]
)
caution
Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop):
If useDynamicAccessTokenSigningKey
is false:
UPDATE session_info SET use_static_key = true;
Else if useDynamicAccessTokenSigningKey
is true:
UPDATE session_info SET use_static_key = false;
If you are using the managed core, you can send an email to us about this, and we will run the query for you.
#
Check for custom claim values for authorizationOnce 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.
#
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.