Skip to main content
FrontMCP provides flexible token and session management with support for both stateful and stateless patterns.

Token Types

TokenPurposeLifetimeStorage
Access TokenAPI authorization1 hour (default)Client-side (JWT)
Refresh TokenObtain new access tokens30 days (default)Server-side
Authorization CodeOAuth flow exchange60 secondsServer-side, single-use
Session TokenTrack user sessionConfigurableDepends on mode

Session Modes

FrontMCP supports two session management strategies:

Stateful Sessions

Tokens stored server-side. Clients hold lightweight references.Pros:
  • Silent token refresh
  • Revocation without client update
  • Secure token storage
Cons:
  • Requires shared storage (Redis)
  • State management complexity

Stateless Sessions

All data embedded in JWT. No server-side storage.Pros:
  • Horizontally scalable
  • No shared state
  • Simple architecture
Cons:
  • No silent refresh
  • Larger token size
  • Can’t revoke without expiry

Stateful Session Configuration

@FrontMcp({
  info: { name: 'MyServer', version: '1.0.0' },
  auth: {
    mode: 'local',
    tokenStorage: {
      redis: {
        host: 'localhost',
        port: 6379,
        keyPrefix: 'myapp:auth:',
      },
    },
  },
})
export class Server {}

Stateless Session Configuration

@FrontMcp({
  info: { name: 'MyServer', version: '1.0.0' },
  auth: {
    mode: 'local',
  },
})
export class Server {}
Stateful sessions require shared storage when running multiple server instances. Without Redis, each instance maintains its own session state.

Token Storage

tokenStorage (on local/remote auth) persists authorization codes, refresh tokens, and federated-auth sessions. Three backends are supported.

In-Memory (Development)

tokenStorage: 'memory' // default
In-memory storage loses all data on restart. Use only for development.

SQLite (Single-Node Persistence)

tokenStorage: {
  sqlite: {
    path: './data/auth.sqlite',          // required
    encryption: { secret: process.env.AUTH_DB_SECRET! }, // optional at-rest encryption
    walMode: true,                       // default true
    ttlCleanupIntervalMs: 60000,         // default 60000
  },
}
Requires @frontmcp/storage-sqlite (lazy-loaded).

Redis (Multi-Instance Persistence)

tokenStorage: {
  redis: {
    host: 'redis.example.com',
    port: 6379,
    password: process.env.REDIS_PASSWORD,
    keyPrefix: 'auth:',
    tls: true,
  },
}

Storage Contents

Key PatternDataTTL
{prefix}pending:{id}Pending authorization10 minutes
{prefix}code:{code}Authorization code60 seconds
{prefix}refresh:{token}Refresh token30 days
{prefix}session:{id}Session dataConfigurable

OrchestratedTokenStore Interface

When using local or remote mode with federated authentication, FrontMCP stores provider tokens using the OrchestratedTokenStore interface.

Interface Methods

MethodDescription
storeTokens(authorizationId, providerId, tokens)Store tokens for a provider
getTokens(authorizationId, providerId)Retrieve tokens for a provider
deleteTokens(authorizationId, providerId)Delete tokens for a provider
getProviderIds(authorizationId)Get all provider IDs with stored tokens
migrateTokens(fromAuthId, toAuthId)Migrate tokens between authorization IDs

Token Migration

The migrateTokens() method is used internally during federated auth completion to transfer tokens from a pending authorization ID to the final authorization ID:
import { InMemoryOrchestratedTokenStore } from '@frontmcp/auth';

const tokenStore = new InMemoryOrchestratedTokenStore();

// During federated auth, tokens are stored with pending ID
await tokenStore.storeTokens('pending:abc123', 'github', {
  accessToken: 'gho_xxx',
  refreshToken: 'ghr_xxx',
});

// After JWT issuance, tokens are migrated to real auth ID
await tokenStore.migrateTokens('pending:abc123', 'user:real-auth-id');

// Query which providers have tokens
const providerIds = await tokenStore.getProviderIds('user:real-auth-id');
// => ['github']
migrateTokens() is called automatically during the OAuth token exchange flow when completing federated authentication.

Token Lifetimes

FrontMCP uses default token lifetimes:
Token TypeDefault Lifetime
Access Token1 hour
Refresh Token30 days
Authorization Code60 seconds

Token Refresh Configuration

auth: {
  mode: 'local',
  refresh: {
    enabled: true,      // Proactively refresh upstream/downstream provider creds
    skewSeconds: 60,    // …this many seconds before THOSE provider tokens expire
  },
}
The refresh block governs proactive refresh of upstream/downstream provider credentials (the tokens read via this.authProviders / this.orchestration.getToken), not the server’s own session token. There is no background refresher for FrontMCP’s own access token: the client refreshes its session token explicitly by calling POST /oauth/token with grant_type=refresh_token (see Token Refresh Flow). Server-issued token lifetimes are fixed at the server level, and refresh tokens are rotated on each use per OAuth 2.1 best practices.

Token Refresh Flow

Refresh tokens are rotated on each use (OAuth 2.1 best practice):

JWT Structure

Access tokens issued by local/remote mode are JWTs signed with HS256 (symmetric, JWT_SECRET). There is no kid because there is no key set to select from:
{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "sub": "user-uuid",
    "iss": "http://localhost:3001",
    "aud": "https://api.myservice.com",
    "exp": 1234567890,
    "iat": 1234567800,
    "jti": "unique-token-id",
    "scope": "read write",
    "email": "user@example.com"
  }
}

Custom Claims

In transparent/remote modes, custom claims are sourced from the upstream identity provider — configure your IdP to include the claims you need (roles, tenant ID, etc.). In local mode, your authenticate() handler can return and customize the claims FrontMCP embeds in the issued token: resolve a { ok: true, sub?, claims? } result and the claims object is merged into the minted access-token payload (namespaced to avoid clobbering reserved claims like sub/exp/iss). Either way, read the claims via this.context.authInfo?.user (raw) or this.auth.claims (typed) inside tools, resources, prompts, and agents.
Enable consent to enforce a per-token authorized-tools claim:
auth: {
  mode: 'local',
  consent: { enabled: true },
}
  1. User authenticates via the built-in login page.
  2. /oauth/callback renders an interactive tool-selection screen listing the available tools (honoring groupByApp, showDescriptions, allowSelectAll, requireSelection, customMessage, excludedTools, defaultSelectedTools).
  3. The user’s checked tools are GET-submitted back to /oauth/callback, embedded in the token’s consent.selectedTools claim, and enforced on every tools/call — an unselected tool is rejected with TOOL_NOT_CONSENTED (JSON-RPC -32003).
Tokens minted without consent (consent disabled, or created via the test/programmatic factory) carry no consent claim and stay all-tools-allowed. excludedTools are always available. rememberConsent (default true) persists each user’s per-client selection and reuses it on the next login, re-prompting only when a NEW tool appears; set it false to always re-show the screen.

Tool-Level Scopes

@Tool({
  name: 'send_message',
  description: 'Send a message',
  scopes: ['messages:write'], // Required scope
})
export class SendMessageTool {
  async execute(ctx: ToolContext) {
    // Only callable if token has messages:write scope
  }
}

Signing Secret (HS256)

local and remote modes sign the tokens they issue with HS256 — a single symmetric secret read from the JWT_SECRET environment variable. The same secret signs and verifies tokens; there is no RSA/EC key pair and no private key to manage.
# Generate a strong secret once and keep it stable.
JWT_SECRET=$(openssl rand -hex 32)
If JWT_SECRET is unset, FrontMCP logs a warning and uses a random per-process secret, so every restart invalidates previously issued tokens. Set a stable JWT_SECRET for any deployment where tokens must survive a restart.

Rotating the secret

Rotating JWT_SECRET immediately invalidates every token signed with the old value — connected clients must re-authenticate. There is no built-in dual-secret overlap window; schedule rotations during a maintenance window or rely on short access-token lifetimes plus refresh.
transparent mode is the asymmetric path: tokens are verified against the upstream IdP’s JWKS (providerConfig.jwksUri / jwks), not a local key. In local/remote mode, /.well-known/jwks.json does publish an auto-generated asymmetric (RS256 by default) public key for OAuth-discovery compatibility — but that key is not used to verify the FrontMCP-issued access tokens, which are HS256 and always verified with JWT_SECRET.

Token Verification

Verification Flow

Verification Options

auth: {
  mode: 'transparent',
  provider: 'https://auth.example.com',
  expectedAudience: 'https://api.myservice.com',
  requiredScopes: ['openid', 'profile'],
}

Error Responses

Token-related errors follow OAuth 2.0 error format:
ErrorHTTP StatusDescription
invalid_token401Token expired, malformed, or invalid signature
insufficient_scope403Token missing required scopes
invalid_request400Malformed token request
invalid_grant400Invalid authorization code or refresh token

Example Error Response

{
  "error": "invalid_token",
  "error_description": "Token has expired",
  "error_uri": "https://tools.ietf.org/html/rfc6750#section-3.1"
}

Next Steps

Authorization Modes

Choose the right auth mode for your use case

Progressive Authorization

Implement incremental app authorization

Production Checklist

Security requirements for deployment

Remote OAuth

Connect to external identity providers