Skip to main content

Overview

SuperTokens Core implements a secure session management system with automatic token rotation, blacklisting support, and anti-CSRF protection. Sessions use JWT-based access tokens and encrypted refresh tokens.

Session Architecture

Token Types

SuperTokens uses three types of tokens:

Access Token

Short-lived JWT containing session data and user information

Refresh Token

Long-lived encrypted token used to generate new access tokens

ID Refresh Token

Client-side indicator for refresh token validity

Access Token Structure

Access tokens are JWTs signed with RS256 containing:
{
  "sessionHandle": "unique-session-id",
  "sub": "primary-user-id",
  "rsub": "recipe-user-id",
  "refreshTokenHash1": "hash-of-refresh-token",
  "parentRefreshTokenHash1": "hash-of-parent-token",
  "antiCsrfToken": "csrf-token",
  "tId": "tenant-id",
  "exp": 1234567890,
  "iat": 1234567890
}
Access tokens support multiple versions (V1-V5) for backwards compatibility. Version 5 includes both primary user ID (sub) and recipe user ID (rsub) for account linking.

Refresh Token Format

Refresh tokens are encrypted and contain:
  • Session handle
  • User ID
  • Parent refresh token hash (for rotation)
  • Anti-CSRF token
  • Nonce for encryption
  • Tenant ID
Format: <encrypted-payload>.<nonce>.V2

Creating Sessions

Sessions are created with:
1

Generate Session Handle

Create a unique UUID for the session, appending tenant ID if not default:
String sessionHandle = UUID.randomUUID().toString();
if (!tenantId.equals(DEFAULT_TENANT_ID)) {
    sessionHandle += "_" + tenantId;
}
2

Create Tokens

Generate refresh token, then create access token with refresh token hash:
TokenInfo refreshToken = RefreshToken.createNewRefreshToken(
    tenantIdentifier, main, sessionHandle, userId, 
    parentRefreshTokenHash, antiCsrfToken
);

TokenInfo accessToken = AccessToken.createNewAccessToken(
    tenantIdentifier, main, sessionHandle, userId, primaryUserId,
    Utils.hashSHA256(refreshToken.token), null, userDataInJWT, 
    antiCsrfToken, null, version, useStaticKey
);
3

Store Session in Database

Persist session with double-hashed refresh token:
storage.createNewSession(
    tenantIdentifier, sessionHandle, userId,
    Utils.hashSHA256(Utils.hashSHA256(refreshToken.token)),
    userDataInDatabase, refreshToken.expiry, 
    userDataInJWT, refreshToken.createdTime, useStaticKey
);

Token Rotation

How Token Rotation Works

SuperTokens implements automatic token rotation to detect token theft:
When a refresh token is used, its hash becomes the “parent” hash. If the parent token is used again, it indicates token theft, and all sessions for that user are revoked.

Refresh Process

From Session.java:543-650:
  1. Verify refresh token and extract session handle
  2. Check anti-CSRF token if enabled
  3. Database transaction:
    • Get session info from database
    • Check if refreshTokenHash2 matches double-hashed input token
    • If match: Create new tokens with current token as parent
    • If parent hash matches: Promote child token (update database)
    • Otherwise: Token theft detected → throw exception

Verifying Sessions

Access Token Verification

The verification process:
1

JWT Signature Verification

Verify JWT signature using public keys from signing key rotation
2

Check Expiry

Validate token hasn’t expired
3

Anti-CSRF Check

Compare anti-CSRF token from header with token in JWT
4

Database Check (Optional)

Verify session hasn’t been blacklisted or revoked
5

Token Promotion

If parentRefreshTokenHash1 exists, promote to current token

Key Verification Code

From AccessToken.java:62-169:
private static AccessTokenInfo getInfoFromAccessToken(
    AppIdentifier appIdentifier, Main main, String token, 
    boolean retry, boolean doAntiCsrfCheck
) {
    // Get all signing keys
    List<JWTSigningKeyInfo> keyInfoList = 
        SigningKeys.getInstance(appIdentifier, main).getAllKeys();
    
    // Pre-parse to get version and kid
    JWT.JWTPreParseInfo preParseJWTInfo = JWT.preParseJWTInfo(token);
    
    // Get specific key by kid
    JWTSigningKeyInfo keyInfo = 
        SigningKeys.getInstance(appIdentifier, main)
            .getSigningKeyById(preParseJWTInfo.kid);
    
    // Verify JWT
    JWT.JWTInfo jwtInfo = JWT.verifyJWTAndGetPayload(
        preParseJWTInfo, keyInfo.publicKey
    );
    
    // Check expiry and anti-CSRF
    if (tokenInfo.expiryTime < System.currentTimeMillis()) {
        throw new TryRefreshTokenException("Access token expired");
    }
    
    return tokenInfo;
}

Session Revocation

Revoke by Session Handle

String[] revokedHandles = Session.revokeSessionUsingSessionHandles(
    main, appIdentifier, storage, new String[]{sessionHandle}
);

Revoke All User Sessions

String[] revokedHandles = Session.revokeAllSessionsForUser(
    main, appIdentifier, storage, userId
);

Session Deletion Process

From Session.java:819-857:
1

Parse Tenant ID

Extract tenant ID from session handle (format: uuid_tenantId)
2

Group by Tenant

Organize session handles by their tenants
3

Delete from Storage

Remove sessions from database for each tenant
4

Return Revoked Sessions

Return list of successfully revoked session handles

Security Features

Anti-CSRF Protection

enableAntiCsrf
boolean
default:"true"
When enabled, generates a random CSRF token stored in both access and refresh tokens
String antiCsrfToken = enableAntiCsrf ? UUID.randomUUID().toString() : null;
The anti-CSRF token is validated on:
  • Session verification (if doAntiCsrfCheck is true)
  • Session refresh

Token Theft Detection

SuperTokens detects token theft when:
  1. A parent refresh token is reused after its child was already used
  2. The system throws TokenTheftDetectedException
  3. All sessions for that user are automatically revoked
From Session.java:652-654:
throw new TokenTheftDetectedException(
    sessionHandle, recipeUserId, primaryUserId
);

Session Blacklisting

Optional database check during verification:
if (checkDatabase) {
    SessionInfo info = storage.getSession(
        tenantIdentifier, sessionHandle
    );
    if (info == null) {
        throw new UnauthorisedException(
            "Session has ended or been blacklisted"
        );
    }
}

Session Data Management

JWT Payload (Client-Accessible)

Stored in access token, available on client:
JsonObject userDataInJWT = new JsonObject();
userDataInJWT.addProperty("role", "admin");
userDataInJWT.addProperty("plan", "premium");
JWT data is visible to the client. Never store sensitive information in the JWT payload.

Database Session Data (Server-Only)

Stored in database, never sent to client:
JsonObject userDataInDatabase = new JsonObject();
userDataInDatabase.addProperty("internalId", "secret-id");

Updating Session Data

SessionInformationHolder session = Session.regenerateToken(
    appIdentifier, main, accessToken, newJWTPayload
);

Configuration

access_token_validity
number
default:"3600"
Access token lifetime in seconds (default: 1 hour)
refresh_token_validity
number
default:"144000"
Refresh token lifetime in minutes (default: 100 days)
access_token_signing_key_update_interval
number
default:"168"
Hours between signing key rotations (default: 7 days)

Multi-Tenancy Support

Sessions are tenant-aware:
  • Session handles include tenant ID: {uuid}_{tenantId}
  • Access tokens contain tId claim
  • Refresh tokens store tenant ID in encrypted payload
  • Session operations are scoped to tenant storage
From Session.java:141-144:
String sessionHandle = UUID.randomUUID().toString();
if (!tenantId.equals(TenantIdentifier.DEFAULT_TENANT_ID)) {
    sessionHandle += "_" + tenantId;
}

Best Practices

Always Use HTTPS

Never transmit tokens over insecure connections

Enable Anti-CSRF

Protect against cross-site attacks for web applications

Use Static Keys Sparingly

Dynamic keys provide better security through rotation

Monitor Token Theft

Log and alert on TokenTheftDetectedException events

Build docs developers (and LLMs) love