Skip to main content

Overview

SuperTokens Core implements multiple layers of security including industry-standard password hashing, JWT signing with key rotation, token encryption, and protection against common attacks.

Password Security

Password Hashing Algorithms

SuperTokens supports multiple password hashing algorithms:

Argon2

Memory-hard algorithm, best security (default)

BCrypt

Industry standard, configurable work factor

Firebase SCrypt

For Firebase migrations

Argon2 Configuration

From io/supertokens/emailpassword/PasswordHashing.java:
argon2_iterations
number
default:"1"
Number of iterations (time cost)
argon2_memory_kb
number
default:"87795"
Memory usage in KB (~85 MB)
argon2_parallelism
number
default:"2"
Number of parallel threads
argon2_hashing_pool_size
number
default:"1"
Thread pool size for hashing operations
// Argon2 hashing
String passwordHash = PasswordHashing.getInstance(main)
    .createHashWithSalt(
        appIdentifier,
        plainTextPassword
    );

// Verification
boolean isValid = PasswordHashing.getInstance(main)
    .verifyPasswordWithHash(
        appIdentifier,
        plainTextPassword,
        passwordHash
    );

BCrypt Configuration

bcrypt_log_rounds
number
default:"11"
Work factor (2^11 = 2048 rounds)
Higher log rounds increase security but slow down authentication. 11 rounds takes approximately 100-200ms.

Firebase SCrypt

For migrating from Firebase:
firebase_password_hashing_signer_key
string
Base64-encoded signer key from Firebase
firebase_password_hashing_salt_separator
string
Base64-encoded salt separator
firebase_password_hashing_rounds
number
default:"8"
Number of rounds used in Firebase
firebase_password_hashing_mem_cost
number
default:"14"
Memory cost parameter

Token Signing

JWT Signing Keys

SuperTokens uses RS256 (RSA with SHA-256) for signing JWTs:
public class JWTSigningKeyInfo {
    public String keyId;           // Unique key identifier
    public String keyString;       // RSA private key (PEM)
    public String publicKey;       // RSA public key (PEM)
    public long createdAtTime;
    public String algorithm;       // "RS256"
}

Key Types

Dynamic Keys

Rotate automatically, used for access tokens by default

Static Keys

Never rotate, used for custom JWTs and special cases

Key Rotation

From io/supertokens/signingkeys/SigningKeys.java:
1

Generate New Key

Create new RSA key pair every access_token_signing_key_update_interval hours
2

Transition Period

Old keys remain valid for token verification during their lifetime
3

Clean Up

Remove expired keys from storage
access_token_signing_key_update_interval
number
default:"168"
Hours between key rotations (default: 7 days)
access_token_dynamic_signing_key_update_interval
number
default:"168"
Hours between dynamic key rotations

JWT Creation

From io/supertokens/jwt/JWTSigningFunctions.java:84-147:
public static String createJWTToken(
    AppIdentifier appIdentifier,
    Main main,
    String algorithm,              // "RS256"
    JsonObject payload,
    String jwksDomain,             // Issuer
    long jwtValidityInSeconds,
    boolean useDynamicKey
) {
    // Get signing key
    JWTSigningKeyInfo keyToUse;
    if (useDynamicKey) {
        keyToUse = SigningKeys.getInstance(appIdentifier, main)
            .getLatestIssuedDynamicKey();
    } else {
        keyToUse = SigningKeys.getInstance(appIdentifier, main)
            .getStaticKeyForAlgorithm(SupportedAlgorithms.RS256);
    }
    
    // Create JWT with headers
    Map<String, Object> headerClaims = new HashMap<>();
    headerClaims.put("alg", "RS256");
    headerClaims.put("typ", "JWT");
    headerClaims.put("kid", keyToUse.keyId);
    
    // Add standard claims
    long issued = System.currentTimeMillis();
    long expires = issued + (jwtValidityInSeconds * 1000);
    
    if (jwksDomain != null) {
        payload.addProperty("iss", jwksDomain);
    }
    
    // Sign and return
    return JWTCreator.create()
        .withKeyId(keyToUse.keyId)
        .withHeader(headerClaims)
        .withIssuedAt(new Date(issued))
        .withExpiresAt(new Date(expires))
        .withPayload(payload.toString())
        .sign(algorithm);
}

JWKS Endpoint

Public keys are exposed via JWKS endpoint:
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86",
      "n": "...",
      "e": "AQAB",
      "alg": "RS256",
      "use": "sig"
    }
  ]
}

Token Encryption

Refresh Token Encryption

Refresh tokens are encrypted using AES-256-CBC:
public static TokenInfo createNewRefreshToken(
    TenantIdentifier tenantIdentifier,
    Main main,
    String sessionHandle,
    String userId,
    String parentRefreshTokenHash,
    String antiCsrfToken
) {
    // Get encryption key
    String key = RefreshTokenKey.getInstance(appIdentifier, main).getKey();
    
    // Create nonce
    String nonce = Utils.hashSHA256(UUID.randomUUID().toString());
    
    // Create payload
    RefreshTokenPayload payload = new RefreshTokenPayload(
        sessionHandle, userId, parentRefreshTokenHash, 
        nonce, antiCsrfToken, tenantId
    );
    
    // Encrypt payload
    String payloadSerialised = new Gson().toJson(payload);
    String encryptedPayload = Utils.encrypt(payloadSerialised, key);
    
    // Format: <encrypted>.<nonce>.V2
    String token = encryptedPayload + "." + nonce + ".V2";
    
    return new TokenInfo(token, expiryTime, createdTime);
}

Encryption Key Storage

Refresh token encryption keys are stored securely in the database and cached in memory.
Never log or expose refresh tokens. They contain encrypted user session data.

Attack Prevention

Token Theft Detection

SuperTokens detects token theft through refresh token rotation: From io/supertokens/session/Session.java:652-654:
if (parentTokenUsed) {
    throw new TokenTheftDetectedException(
        sessionHandle, 
        recipeUserId, 
        primaryUserId
    );
}

Anti-CSRF Protection

enable_anti_csrf
boolean
default:"true"
Enable anti-CSRF token validation
Anti-CSRF tokens:
  • Generated as random UUIDs
  • Stored in both access and refresh tokens
  • Validated on session verification and refresh
  • Sent as separate header or cookie
// Generate CSRF token
String antiCsrfToken = enableAntiCsrf 
    ? UUID.randomUUID().toString() 
    : null;

// Verify CSRF token
if (enableAntiCsrf && doAntiCsrfCheck) {
    if (antiCsrfToken == null || 
        !antiCsrfToken.equals(accessToken.antiCsrfToken)) {
        throw new TryRefreshTokenException("anti-csrf check failed");
    }
}

Session Blacklisting

Optional database verification to detect revoked sessions:
if (checkDatabase) {
    SessionInfo sessionInfo = storage.getSession(
        tenantIdentifier, 
        accessToken.sessionHandle
    );
    
    if (sessionInfo == null) {
        throw new UnauthorisedException(
            "Session has ended or has been blacklisted"
        );
    }
}
Database checks add latency but provide immediate session invalidation. Without them, revoked sessions remain valid until access token expiry.

Rate Limiting

max_server_pool_size
number
default:"10"
Maximum concurrent requests per core instance
Built-in connection pooling prevents resource exhaustion.

Secure Storage

Password Reset Tokens

Password reset tokens are:
  • Randomly generated UUIDs
  • Hashed before storage using SHA-256
  • Single-use only
  • Time-limited (configurable expiry)
password_reset_token_lifetime
number
default:"3600000"
Reset token lifetime in milliseconds (default: 1 hour)

Email Verification Tokens

Similar to password reset tokens:
  • UUID-based
  • Hashed in database
  • Single-use
  • Time-limited
email_verification_token_lifetime
number
default:"86400000"
Verification token lifetime in milliseconds (default: 24 hours)

Cryptographic Operations

Hashing Utilities

From io/supertokens/utils/Utils.java:
// SHA-256 hashing
public static String hashSHA256(String input) {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
    return bytesToHex(hash);
}

// Double hashing for refresh tokens
String doubleHash = Utils.hashSHA256(
    Utils.hashSHA256(refreshToken)
);

AES Encryption/Decryption

// Encrypt data
public static String encrypt(String data, String key) 
    throws Exception {
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    SecretKeySpec secretKey = new SecretKeySpec(
        key.getBytes(), "AES"
    );
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    byte[] encrypted = cipher.doFinal(data.getBytes());
    return Base64.getEncoder().encodeToString(encrypted);
}

// Decrypt data
public static String decrypt(String encryptedData, String key) 
    throws Exception {
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    SecretKeySpec secretKey = new SecretKeySpec(
        key.getBytes(), "AES"
    );
    cipher.init(Cipher.DECRYPT_MODE, secretKey);
    byte[] decoded = Base64.getDecoder().decode(encryptedData);
    byte[] decrypted = cipher.doFinal(decoded);
    return new String(decrypted);
}

Database Security

SQL Injection Prevention

All database queries use parameterized statements:
// Safe - parameterized query
PreparedStatement pst = con.prepareStatement(
    "SELECT * FROM users WHERE email = ?"
);
pst.setString(1, email);

// NEVER do this:
// String query = "SELECT * FROM users WHERE email = '" + email + "'";

Connection Pooling

postgresql_connection_pool_size
number
default:"10"
PostgreSQL connection pool size
mysql_connection_pool_size
number
default:"10"
MySQL connection pool size
Connection pooling prevents:
  • Connection exhaustion attacks
  • Resource leaks
  • Performance degradation

Configuration Security

Protected Configurations

From io/supertokens/multitenancy/Multitenancy.java:174-179: These configs cannot be changed after tenant creation:
  • Database connection parameters
  • Core service ports
  • Base paths
  • API keys
  • Signing keys
for (String protectedConfig : CoreConfig.PROTECTED_CONFIGS) {
    if (tenantConfig.coreConfig.has(protectedConfig) &&
            !tenantConfig.coreConfig.get(protectedConfig)
                .equals(currentConfig.get(protectedConfig))) {
        throw new BadPermissionException(
            "Not allowed to modify protected configs."
        );
    }
}

API Key Security

api_keys
string
Comma-separated list of API keys for core authentication
api_keys: "key1,key2,key3"
API keys must be:
  • At least 20 characters
  • Randomly generated
  • Stored securely (environment variables, secrets manager)
  • Rotated periodically
Never commit API keys to version control. Use environment variables or secret management systems.

Security Headers

SuperTokens sets secure HTTP headers:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains

CORS Configuration

supertokens_saas_allowed_domains
string
Comma-separated list of allowed domains for CORS
supertokens_saas_allowed_domains: "https://example.com,https://app.example.com"

Audit Logging

Enable detailed logging for security audits:
log_level
string
default:"INFO"
Logging level: DEBUG, INFO, WARN, ERROR
info_log_path
string
Path to info log file
error_log_path
string
Path to error log file

Best Practices

Use Argon2

Default password hashing algorithm provides best security

Enable Key Rotation

Use dynamic keys with regular rotation intervals

Enable Anti-CSRF

Always enable for web applications

Monitor for Theft

Log TokenTheftDetectedException events

Use HTTPS Only

Never transmit tokens over unencrypted connections

Rotate API Keys

Periodically update API keys and remove old ones

Security Checklist

1

Configure Password Hashing

Set appropriate Argon2 or BCrypt parameters for your use case
2

Set Token Lifetimes

Balance security and user experience with token validity periods
3

Enable HTTPS

Ensure all communication uses TLS 1.2 or higher
4

Configure CORS

Whitelist only trusted domains
5

Set API Keys

Use strong, random API keys and rotate them regularly
6

Enable Logging

Configure audit logging for security events
7

Review Permissions

Follow principle of least privilege for tenant operations

Build docs developers (and LLMs) love