Skip to main content

Overview

SuperTokens Core provides a complete OAuth 2.0 implementation that can function as both an authorization server and OAuth client. The implementation is built on top of Ory Hydra and includes token management, introspection, and revocation capabilities.

OAuth 2.0 as Authorization Server

SuperTokens can act as an OAuth 2.0 authorization server, allowing your application to issue OAuth tokens to third-party applications.

Client Management

Create or Update OAuth Client

Implementation: io.supertokens.oauth.OAuth.addOrUpdateClient() - View source API Endpoint: POST /recipe/oauth/clients Request Body:
{
  "clientId": "my-oauth-client",
  "clientSecret": "secret-value",
  "isClientCredentialsOnly": false,
  "enableRefreshTokenRotation": true
}
Response:
{
  "status": "OK",
  "clientId": "my-oauth-client",
  "isClientCredentialsOnly": false,
  "enableRefreshTokenRotation": true
}
Features:
  • Client secret encryption at rest
  • Support for client credentials grant
  • Refresh token rotation
  • Multi-tenancy support

List OAuth Clients

API Endpoint: GET /recipe/oauth/clients?clientIds=client1,client2 Response:
{
  "status": "OK",
  "clients": [
    {
      "clientId": "client1",
      "clientSecret": "decrypted-secret",
      "isClientCredentialsOnly": false,
      "enableRefreshTokenRotation": true
    }
  ]
}

Remove OAuth Client

API Endpoint: POST /recipe/oauth/clients/remove Request Body:
{
  "clientId": "my-oauth-client"
}

Authorization Flow

Authorization Request

Initiate the OAuth authorization flow. API Endpoint: GET /recipe/oauth/auth Query Parameters:
  • client_id: OAuth client identifier
  • redirect_uri: Callback URL
  • response_type: “code” for authorization code flow
  • scope: Requested scopes (space-separated)
  • state: CSRF protection token
  • code_challenge: PKCE challenge (optional)
  • code_challenge_method: “S256” for SHA-256 (optional)
Example:
GET /recipe/oauth/auth?client_id=my-client&redirect_uri=https://app.com/callback&response_type=code&scope=openid%20email&state=random-state

Token Exchange

Exchange authorization code for access tokens. Implementation: io.supertokens.oauth.OAuth.doOAuthProxyFormPOST() - View source API Endpoint: POST /recipe/oauth/token Request Body (Authorization Code Grant):
{
  "iss": "https://your-domain.com",
  "inputBody": {
    "grant_type": "authorization_code",
    "code": "authorization-code",
    "redirect_uri": "https://app.com/callback",
    "client_id": "my-client",
    "client_secret": "client-secret",
    "code_verifier": "pkce-verifier"
  },
  "access_token": {
    "custom_claim": "value"
  },
  "id_token": {
    "custom_claim": "value"
  },
  "useStaticSigningKey": true
}
Response:
{
  "access_token": "signed-jwt-access-token",
  "id_token": "signed-jwt-id-token",
  "refresh_token": "st_rt_xxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid email"
}
Request Body (Client Credentials Grant):
{
  "iss": "https://your-domain.com",
  "inputBody": {
    "grant_type": "client_credentials",
    "client_id": "my-client",
    "client_secret": "client-secret",
    "scope": "api:read"
  },
  "access_token": {},
  "useStaticSigningKey": true
}
Request Body (Refresh Token Grant):
{
  "iss": "https://your-domain.com",
  "inputBody": {
    "grant_type": "refresh_token",
    "refresh_token": "st_rt_xxx",
    "client_id": "my-client",
    "client_secret": "client-secret"
  },
  "access_token": {},
  "useStaticSigningKey": true
}

Token Management

Token Introspection

Validate and inspect access tokens. Implementation: io.supertokens.oauth.OAuth.introspectAccessToken() - View source API Endpoint: POST /recipe/oauth/introspect Request Body:
{
  "token": "access-token"
}
Response (Active Token):
{
  "active": true,
  "sub": "user-id",
  "client_id": "my-client",
  "token_type": "Bearer",
  "token_use": "access_token",
  "scope": "openid email",
  "exp": 1234567890,
  "iat": 1234567800,
  "jti": "token-id",
  "gid": "session-id"
}
Response (Revoked/Invalid Token):
{
  "active": false
}

Token Revocation

Revoke access tokens, refresh tokens, or entire sessions. Revoke Refresh Token: Implementation: io.supertokens.oauth.OAuth.revokeRefreshToken() - View source POST /recipe/oauth/tokens/revoke
{
  "gid": "session-id"
}
Revoke Access Token: Implementation: io.supertokens.oauth.OAuth.revokeAccessToken() - View source POST /recipe/oauth/token/revoke
{
  "token": "access-token"
}
Revoke Session: Implementation: io.supertokens.oauth.OAuth.revokeSessionHandle() - View source POST /recipe/oauth/session/revoke
{
  "sessionHandle": "session-handle"
}
Revoke by Client ID: Implementation: io.supertokens.oauth.OAuth.revokeTokensForClientId() - View source POST /recipe/oauth/tokens/revoke
{
  "clientId": "my-client"
}

Token Structure

Access Token

Access tokens are signed JWTs with the following structure: Header:
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-id"
}
Payload:
{
  "sub": "user-id",
  "iss": "https://your-domain.com",
  "aud": ["client-id"],
  "exp": 1234567890,
  "iat": 1234567800,
  "jti": "unique-token-id",
  "gid": "session-id",
  "stt": 1,
  "client_id": "my-client",
  "scope": "openid email",
  "custom_claim": "custom_value"
}
Claims:
  • sub: Subject (user ID)
  • iss: Issuer (your domain)
  • aud: Audience (client IDs)
  • exp: Expiration time
  • iat: Issued at time
  • jti: JWT ID (unique token identifier)
  • gid: Group ID (session identifier)
  • stt: SuperTokens token type (1 = access token)
  • client_id: OAuth client ID
  • scope: Granted scopes

ID Token

ID tokens are signed JWTs containing user identity information: Payload:
{
  "sub": "user-id",
  "iss": "https://your-domain.com",
  "aud": "client-id",
  "exp": 1234567890,
  "iat": 1234567800,
  "auth_time": 1234567800,
  "nonce": "request-nonce",
  "at_hash": "access-token-hash",
  "email": "[email protected]",
  "email_verified": true,
  "name": "John Doe"
}
Standard Claims:
  • sub: Subject (user ID)
  • iss: Issuer
  • aud: Audience (client ID)
  • exp: Expiration
  • iat: Issued at
  • auth_time: Authentication time
  • nonce: Request nonce
  • at_hash: Access token hash (for implicit/hybrid flows)

Refresh Token

Refresh tokens are opaque strings with the format:
st_rt_<random-string>
Features:
  • Long-lived (configurable)
  • One-time use (with rotation)
  • Mapped to internal Hydra tokens
  • Revocable by session or client

Token Signing

Static Signing Key

Use a static RSA key for token signing:
{
  "useStaticSigningKey": true
}
Benefits:
  • Consistent key ID
  • Simpler key management
  • Better caching

Dynamic Signing Key

Use rotating keys for enhanced security:
{
  "useStaticSigningKey": false
}
Benefits:
  • Key rotation
  • Compromised key isolation
  • Enhanced security

Client Secret Encryption

Client secrets are encrypted at rest using AES-256-GCM. Configuration:
oauth_client_secret_encryption_key: "base64-encoded-key"
Implementation: See also: io.supertokens.oauth.OAuth.encryptClientSecret() - View source

PKCE Support

Proof Key for Code Exchange (PKCE) is supported for public clients. Flow:
  1. Client generates code verifier:
const codeVerifier = generateRandomString(128);
  1. Client creates code challenge:
const codeChallenge = base64url(sha256(codeVerifier));
  1. Authorization request includes challenge:
GET /recipe/oauth/auth?
  client_id=my-client&
  code_challenge=<challenge>&
  code_challenge_method=S256
  1. Token request includes verifier:
{
  "grant_type": "authorization_code",
  "code": "auth-code",
  "code_verifier": "<verifier>"
}

Logout Flow

Implementation: io.supertokens.oauth.OAuth.createLogoutRequestAndReturnRedirectUri() - View source Create Logout Request: POST /recipe/oauth/logout Request Body:
{
  "clientId": "my-client",
  "postLogoutRedirectionUri": "https://app.com/logged-out",
  "sessionHandle": "session-handle",
  "state": "random-state"
}
Response:
{
  "status": "OK",
  "redirectUri": "{apiDomain}/oauth/logout?logout_challenge=<challenge>"
}
Consume Logout Challenge: Implementation: io.supertokens.oauth.OAuth.consumeLogoutChallengeAndGetRedirectUri() - View source

Configuration

OAuth Provider URLs

# Hydra public service URL
oauth_provider_public_service_url: http://localhost:4444

# Hydra admin service URL  
oauth_provider_admin_service_url: http://localhost:4445

# Client secret encryption key (base64)
oauth_client_secret_encryption_key: "your-encryption-key"

Security Features

Token Revocation

Tokens can be revoked:
  • By JTI (access token ID)
  • By GID (session ID)
  • By client ID
  • By session handle
Revocation Checking: See also: io.supertokens.oauth.OAuth.isTokenRevokedBasedOnPayload() - View source

State Parameter

Always validate state parameter to prevent CSRF:
const state = generateRandomString();
saveToSession(state);
// Later verify
if (receivedState !== savedState) {
  throw new Error("Invalid state");
}

Redirect URI Validation

Redirect URIs must:
  • Match exactly (no wildcards)
  • Be registered with the client
  • Use HTTPS in production

M2M Token Tracking

Track machine-to-machine (client credentials) tokens for analytics. Implementation: io.supertokens.oauth.OAuth.addM2MToken() - View source

Error Handling

OAuth Errors

Implemented in io.supertokens.oauth.exceptions.OAuthAPIException. Common Errors:
  • invalid_request: Malformed request
  • invalid_client: Client authentication failed
  • invalid_grant: Invalid authorization code or refresh token
  • unauthorized_client: Client not authorized for grant type
  • unsupported_grant_type: Grant type not supported
  • invalid_scope: Requested scope is invalid
  • access_denied: User denied authorization

Best Practices

  1. Use PKCE: Always use PKCE for public clients
  2. Rotate secrets: Regularly rotate client secrets
  3. Short-lived tokens: Keep access tokens short-lived (15-60 minutes)
  4. Refresh token rotation: Enable rotation for security
  5. Validate state: Always validate state parameter
  6. HTTPS only: Never use OAuth over HTTP in production
  7. Minimal scopes: Request only necessary scopes
  8. Revoke on logout: Revoke tokens when user logs out
  9. Monitor usage: Track token usage for security
  10. Encrypt secrets: Store client secrets encrypted

Multi-Tenancy

OAuth clients are tenant-specific:
  • Each tenant has isolated OAuth clients
  • Token validation respects tenant boundaries
  • Client IDs must be unique per tenant
  • Tokens include tenant context

Build docs developers (and LLMs) love