> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Authorization Modes

> Deep dive into public, transparent, local, and remote authentication modes

FrontMCP's five authentication modes address different deployment scenarios. Understanding when to use each mode is critical for both security and developer experience.

## Mode Overview

The `auth.mode` literal accepts one of five values: `'public'`, `'static'`, `'transparent'`, `'local'`, `'remote'`.

```mermaid theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
graph TB
    subgraph "Authentication Modes"
        Public["mode: 'public'<br/>Anonymous Access"]
        Static["mode: 'static'<br/>Shared Access Token / API Key"]
        Transparent["mode: 'transparent'<br/>Pass-through Tokens"]
        Local["mode: 'local'<br/>Built-in OAuth Server"]
        Remote["mode: 'remote'<br/>OAuth Server proxying to upstream IdP"]
    end
```

## Mode Comparison

| Feature | Public | Static | Transparent | Local | Remote |
| - | - | - | - | - | - |
| Token Required | No | Yes (shared secret) | Yes (external) | Yes (FrontMCP-issued) | Yes (FrontMCP-issued) |
| User Identity | Anonymous | Per configured token | From upstream IdP | From login form | From upstream IdP |
| Token Signing | HS256 (symmetric) | None — opaque secret | Upstream IdP | HS256 (symmetric, `JWT_SECRET`) | HS256 (symmetric, `JWT_SECRET`) |
| Session Management | Minimal | Minimal | Pass-through | Full control | Full control |
| Multi-provider | No | No | Single provider | Multiple via apps | Multiple via apps |
| Progressive Auth | No | No | No | Yes | Yes |
| Tool-authz enforcement | No | No | No | Optional | Optional |

***

## Public Mode

No authentication required. All requests receive an anonymous session.

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
const auth: AuthOptionsInput = {
  mode: 'public',
  sessionTtl: 3600, // 1 hour
  anonymousScopes: ['anonymous'],
};
```

### How It Works

```mermaid theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
sequenceDiagram
    participant Client
    participant FrontMCP

    Client->>FrontMCP: Request without token
    FrontMCP->>FrontMCP: Generate anonymous JWT
    FrontMCP-->>Client: Response + anonymous session
```

### Configuration Options

| Option | Type | Default | Description |
| - | - | - | - |
| `sessionTtl` | `number` | `3600` | Session lifetime in seconds |
| `anonymousScopes` | `string[]` | `['anonymous']` | Scopes assigned to anonymous sessions |
| `publicAccess.tools` | `string[] \| 'all'` | `'all'` | Tools accessible without auth |
| `publicAccess.prompts` | `string[] \| 'all'` | `'all'` | Prompts accessible without auth |
| `publicAccess.rateLimit` | `number` | `60` | Rate limit per IP per minute |

### Use Cases

<CardGroup cols={2}>
  <Card title="Development" icon="code">
    Rapid prototyping without auth setup overhead
  </Card>

  <Card title="Public APIs" icon="globe">
    Endpoints that don't require user identity
  </Card>
</CardGroup>

<Warning>
  **Do NOT use public mode when you need:**

  * User identity tracking
  * Audit trails
  * Access control per user
  * Compliance requirements
</Warning>

<Note>
  **Bearer tokens in public mode.** A JWT is still verified against this instance's own HS256 secret, so a valid gateway token upgrades the request to that identity. A bearer token that is *not* a JWT is ignored and the request is served anonymously — public mode has no issuer, JWKS or audience to verify it against, and a request carrying a credential must never fare worse than the same request without one.

  That also means a `@Will('checkAuthorization')` hook can implement its own shared-secret check without having to strip the header before the built-in verifier sees it. For a first-class shared secret, prefer [static mode](#static-mode).
</Note>

<Warning>
  **The session id is the anonymous caller's only credential.** Every anonymous session shares an empty token, so the server honors an `mcp-session-id` only when it decrypts under this deployment's `MCP_SESSION_SECRET`. An id minted under another secret — another deployment sharing the Redis, or a secret you rotated — is answered with HTTP 404 and the client re-initializes. Run every instance with the same `MCP_SESSION_SECRET`: any of them then serves a session another one minted. A client that sends `initialize` again on a different instance starts a new session there, rather than a second transport for the old id.
</Warning>

***

## Static Mode

A fixed shared secret presented on every request — the shape every non-OAuth MCP host expects. ChatGPT's custom-app connector calls it **Access token / API key**: it attaches a constant `Authorization: Bearer <token>` and never performs an OAuth exchange.

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
const auth: AuthOptionsInput = {
  mode: 'static',
  tokens: [process.env.MCP_AUTH_TOKEN!],
};
```

Use it when you want a server protected by one secret without standing up an OAuth 2.1 provider. Before this mode existed the only options were "no auth at all" or a full OAuth server — and for a server wrapping a billed third-party API, "no auth" means anyone who learns the URL can spend your money.

### How It Works

1. The credential is read from the configured header (`authorization` by default), and the scheme prefix (`Bearer` by default, matched case-insensitively) is stripped.
2. It is compared against each configured token **in constant time**, over SHA-256 digests, so neither the secret's value nor its length is observable through timing.
3. A match produces an authenticated session whose `sub` is `static:<12 hex chars>` — a non-reversible digest prefix of the token that matched, so audit logs and per-subject partitions (rate limits, sessions) can tell configured tokens apart without the secret appearing anywhere.
4. Anything else answers `401` with a `WWW-Authenticate: Bearer realm="…"` challenge. Unlike public mode, a **missing** credential is also a 401.

### Configuration Options

| Option | Type | Default | Description |
| - | - | - | - |
| `tokens` | `string[]` | Required | Accepted credentials. At least one. Read them from the environment. |
| `header` | `string` | `'authorization'` | Request header carrying the credential. |
| `scheme` | `string` | `'Bearer'` | Scheme prefix stripped before comparing. `''` for a bare token header. |
| `scopes` | `string[]` | `['static']` | Scopes granted to an accepted request. |
| `realm` | `string` | `'mcp'` | Realm reported in the `WWW-Authenticate` challenge. |
| `publicAccess` | object | - | Same tool/prompt access configuration the other modes accept. |

An API-key header with no scheme prefix:

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
const auth: AuthOptionsInput = {
  mode: 'static',
  tokens: [process.env.MCP_API_KEY!],
  header: 'x-api-key',
  scheme: '',
};
```

<Warning>
  A shared secret identifies a **caller**, not a user. There is no per-user identity, no revocation short of rotating the token and redeploying, and no progressive auth. Use `local` or `remote` when you need any of those. Rotate by deploying with both the old and new token in `tokens`, then removing the old one.
</Warning>

<Note>
  Static mode is checked entirely in the `session:verify` flow's `handleStaticToken` stage, before any other authentication stage runs. The server still exposes the OAuth discovery endpoints that public mode does; nothing uses them in this mode.
</Note>

***

## Transparent Mode

Pass-through tokens from an external identity provider. FrontMCP validates tokens but doesn't issue them.

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
const auth: AuthOptionsInput = {
  mode: 'transparent',
  provider: 'https://auth.example.com',
  providerConfig: {
    jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  },
  expectedAudience: 'https://api.myservice.com',
  requiredScopes: ['openid'],
  allowAnonymous: false,
};
```

### How It Works

```mermaid theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
sequenceDiagram
    participant Client
    participant FrontMCP
    participant IdP as Identity Provider

    Client->>IdP: Authenticate
    IdP-->>Client: Access Token (JWT)
    Client->>FrontMCP: Request + Bearer token
    FrontMCP->>IdP: Fetch JWKS (cached)
    FrontMCP->>FrontMCP: Verify JWT signature
    FrontMCP->>FrontMCP: Validate claims (iss, aud, exp, scopes)
    FrontMCP-->>Client: Authorized response
```

### Configuration Options

| Option | Type | Default | Description |
| - | - | - | - |
| `provider` | `string` | Required | Base URL of the IdP |
| `providerConfig.jwksUri` | `string` | Auto-discovered | Custom JWKS endpoint |
| `providerConfig.jwks` | `JSONWebKeySet` | - | Inline JWKS for offline verification |
| `providerConfig.additionalIssuers` | `string[]` | - | Extra `iss` values to trust beyond `provider` (e.g. a gateway that re-mints tokens). Trusted verbatim — set only to issuers you control |
| `providerConfig.verifyIssuer` | `boolean` | `true` | Validate the token `iss` claim. Setting `false` **disables issuer checking entirely** — see the security note below |
| `expectedAudience` | `string \| string[]` | Resource URL | Required audience claim value(s) — typically the protected resource URL. Defaults to the derived resource URL when omitted |
| `requiredScopes` | `string[]` | `[]` | Scopes that must be present |
| `allowAnonymous` | `boolean` | `false` | Allow requests without tokens |

### Claim validation

A valid signature only proves the IdP's key signed the token — not that the token
was minted for **this** server. Because every service behind the same IdP shares
the same signing keys, FrontMCP enforces two claims on top of the signature:

* **Issuer (`iss`)** — must equal `provider` (matched with or without a trailing
  slash) or one of `providerConfig.additionalIssuers`. Enabled by default. This
  stops a token minted by the same IdP for a **different issuer** from being
  replayed here.
* **Audience (`aud`)** — if the token carries an `aud`, it must match
  `expectedAudience` (or the derived resource URL). This stops a token issued for
  **service A** from being replayed against **service B**. Tokens with no `aud`
  claim are accepted for IdP compatibility; set `expectedAudience` and issue
  audience-bound tokens for the strictest posture.
* **Expiry (`exp`)** — required. A token without `exp` would never expire, so it
  is refused (`401` whose `error_description` says `exp` is missing).

<Warning>
  **`providerConfig.verifyIssuer: false` disables issuer validation entirely.**
  Any token signed by a key in the provider JWKS is then accepted regardless of
  its `iss`. Only use it for a trusted gateway that re-mints tokens under an
  issuer you cannot enumerate with `additionalIssuers`, and always pair it with a
  strict `expectedAudience`. Whenever the issuer set is known, prefer
  `additionalIssuers` over disabling the check.
</Warning>

### Provider Examples

<Tabs>
  <Tab title="Auth0">
    ```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    auth: {
      mode: 'transparent',
      provider: 'https://your-tenant.auth0.com',
      // JWKS discovered automatically from /.well-known/jwks.json
      expectedAudience: 'https://api.yourservice.com',
    }
    ```
  </Tab>

  <Tab title="Okta">
    ```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    auth: {
      mode: 'transparent',
      provider: 'https://your-org.okta.com/oauth2/default',
      expectedAudience: 'api://default',
    }
    ```
  </Tab>

  <Tab title="Azure AD">
    ```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    auth: {
      mode: 'transparent',
      provider: 'https://login.microsoftonline.com/{tenant}/v2.0',
      providerConfig: {
        jwksUri: 'https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys',
      },
      expectedAudience: 'api://{client-id}',
    }
    ```
  </Tab>
</Tabs>

### Use Cases

<CardGroup cols={2}>
  <Card title="Existing IdP Integration" icon="plug">
    Your organization already uses Auth0, Okta, or similar
  </Card>

  <Card title="Single Provider" icon="1">
    All users authenticate through one identity provider
  </Card>
</CardGroup>

<Warning>
  **Do NOT use transparent mode when you need:**

  * Multiple identity providers
  * Progressive authorization (add apps over time)
  * Server-side token storage with silent refresh
  * Custom token claims
</Warning>

***

## Local Mode

FrontMCP acts as a full OAuth 2.1 authorization server with a built-in login form. Self-contained auth server with built-in user management.

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
const auth: AuthOptionsInput = {
  mode: 'local',
  // 'memory' (default) is lost on restart. Use { sqlite: { path } } for
  // single-node persistence or { redis: ... } for multi-instance.
  tokenStorage: 'memory',
};
```

Local mode signs the tokens it issues with **HS256** using the `JWT_SECRET` environment variable (no RSA/EC key pair). See [Local OAuth](/frontmcp/authentication/local) for token signing, persistent `tokenStorage` (memory / sqlite / redis), the `requireEmail` / `anonymousSubject` single-operator options, and tunnel/issuer configuration.

Local mode also orchestrates **multiple upstream OAuth providers** out of the box. Declare a `providers` array (GitHub, Slack, Jira, …) and FrontMCP federates them at `/oauth/authorize`, refuses to mint a JWT until `federatedAuth.minProviders` (default `1`) are linked, stores each provider's tokens encrypted server-side, and exposes them to tools via `this.orchestration.getToken(id)`:

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
const auth: AuthOptionsInput = {
  mode: 'local',
  providers: [
    { id: 'github', authorizeUrl: '…', tokenUrl: '…', clientId: '…', scopes: ['repo'] },
    { id: 'slack', authorizeUrl: '…', tokenUrl: '…', clientId: '…' },
  ],
  federatedAuth: { minProviders: 1 }, // no JWT until ≥1 linked
};
```

See [Multi-Provider Orchestration](/frontmcp/authentication/local#multi-provider-orchestration-providers) for the full provider schema, the `minProviders` / `requiredProviders` gate, and the `this.orchestration` tool API.

<Warning>
  The built-in login page accepts any email format without validation and is intended for development. Replace with a real identity provider for production use.
</Warning>

## Remote Mode

FrontMCP acts as an OAuth 2.1 authorization server that **proxies user authentication to an upstream IdP**. End users never submit credentials to FrontMCP directly — they're redirected to the upstream IdP, and FrontMCP exchanges the resulting upstream code/tokens before issuing its own session token to the MCP client.

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
const auth: AuthOptionsInput = {
  mode: 'remote',
  provider: 'https://auth.example.com',
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret',
  scopes: ['openid', 'profile', 'email'],
  consent: { enabled: true },
};
```

### How It Works

```mermaid theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
sequenceDiagram
    participant User
    participant Client
    participant FrontMCP as FrontMCP Auth Server
    participant IdP as Upstream IdP
    participant Store as Authorization Store

    Note over Client: Generate PKCE<br/>code_verifier & code_challenge

    Client->>FrontMCP: GET /oauth/authorize<br/>response_type=code<br/>client_id, redirect_uri<br/>code_challenge (S256)<br/>scope, state

    FrontMCP->>Store: Store pending authorization
    FrontMCP-->>User: 302 Redirect to upstream IdP<br/>(authorize URL + upstream PKCE)

    User->>IdP: Authenticate (credentials, MFA, …)
    IdP-->>FrontMCP: Redirect back with upstream auth code
    FrontMCP->>IdP: POST /token (exchange upstream code)
    IdP-->>FrontMCP: { id_token, access_token, refresh_token }

    FrontMCP->>Store: Create FrontMCP authorization code<br/>(60s TTL, single-use)
    FrontMCP-->>Client: Redirect to redirect_uri<br/>?code=xxx&state=yyy

    Client->>FrontMCP: POST /oauth/token<br/>grant_type=authorization_code<br/>code, redirect_uri<br/>client_id, code_verifier

    FrontMCP->>Store: Get & validate code
    FrontMCP->>FrontMCP: Verify PKCE<br/>SHA256(code_verifier) == code_challenge
    FrontMCP->>Store: Mark code as used
    FrontMCP->>FrontMCP: Sign FrontMCP session token (JWT)<br/>using the upstream identity
    FrontMCP->>Store: Store refresh token (links to upstream tokens)
    FrontMCP-->>Client: { access_token, refresh_token, expires_in }
```

The upstream identity comes from the IdP's `id_token` only when it verifies:
signed by a key the provider publishes (`providerConfig.jwks`, else
`providerConfig.jwksUri`, else `<provider>/.well-known/jwks.json`, else the
`jwks_uri` of the provider's OAuth metadata or `/.well-known/openid-configuration`), issued by
`provider` (or `providerConfig.additionalIssuers`), for `clientId`, and not
expired. Otherwise FrontMCP asks the userinfo endpoint. A callback whose RFC 9207
`iss` names another server, and a sign-in the user declined at the IdP
(`error=access_denied`), end with an error page and no code.

### Configuration Options

| Option | Type | Default | Description |
| - | - | - | - |
| `requireRegisteredClients` | `boolean` | `true` | Refuse an MCP client that isn't registered (DCR) or a CIMD URL. In production remote mode only CIMD clients qualify (see below) |
| `allowedScopes` | `string[]` | OpenID scopes | Scopes FrontMCP may grant in its own tokens (see Local docs). Unrelated to `scopes`, which are requested from the IdP |
| `expectedAudience` | `string \| string[]` | Resource URL | Audiences (`aud`) whose tokens FrontMCP accepts, checked in place of the URL the request arrived at |
| `consent` | `ConsentConfig` | `undefined` (block optional) | Tool-selection consent screen + call-time enforcement (see Local docs). When the block is present, inner `enabled` defaults to `false`. |
| `tokenStorage` | `'memory' \| { redis: RedisConfig } \| { sqlite: SqliteConfig }` | `'memory'` | Storage backend (memory / sqlite / redis) |
| `allowDefaultPublic` | `boolean` | `false` | Allow unauthenticated requests |
| `federatedAuth` | `FederatedAuthConfig` | - | Federated auth state validation |
| `incrementalAuth` | `IncrementalAuthConfig` | `undefined` (block optional) | Progressive authorization. When the block is present, inner `enabled` defaults to `true`. |

<Warning>
  **Remote mode and unregistered clients.** Remote mode has no `dcr` block (no
  pre-registered clients) and its `/oauth/register` is off in production, so with
  the default `requireRegisteredClients: true` a production remote server admits
  only MCP clients that use a [CIMD](/frontmcp/authentication/cimd) client-id
  URL. In 1.8.2 and earlier an unregistered `client_id` was accepted; see [MCP client
  registration](/frontmcp/authentication/remote#mcp-client-registration) for the
  migration.
</Warning>

### Consent Configuration

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
consent: {
  enabled: true,
  groupByApp: true,
  showDescriptions: true,
  allowSelectAll: true,
  requireSelection: true,
  excludedTools: ['ping'],          // always available, never gated
  defaultSelectedTools: ['create-note'], // pre-checked on the screen
}
```

When `consent.enabled` is `true`, the local flow renders an **interactive tool-selection screen** after login and **enforces the selection at call time**: the chosen tool ids are embedded in the token's `consent` claim and a `tools/call` to an unselected tool is rejected with `TOOL_NOT_CONSENTED` (JSON-RPC `-32003`). The flags `groupByApp`, `showDescriptions`, `allowSelectAll`, `requireSelection`, `customMessage`, `excludedTools`, and `defaultSelectedTools` are all honored. See [Local OAuth → Consent](/frontmcp/authentication/local#consent-and-tool-authorization) for the full table.

<Note>
  Tokens minted without consent (consent disabled, or created via the test/programmatic factory) carry no `consent` claim and are unaffected — all tools remain callable. `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.
</Note>

### Incremental Authorization

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
incrementalAuth: {
  enabled: true,
  allowSkip: true,                    // Allow skipping app auth
  showAllAppsAtOnce: true,            // Show all apps in one page
  skippedAppBehavior: 'require-auth', // 'anonymous' or 'require-auth'
}
```

### Federated Authentication Configuration

Configure how multi-provider (federated) authentication is gated and validated.

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
federatedAuth: {
  stateValidation: 'strict',     // 'strict' (recommended) or 'format'
  minProviders: 1,               // no JWT until ≥1 provider linked
  requiredProviders: ['github'], // these ids must all be linked
}
```

| Option | Values | Description |
| - | - | - |
| `stateValidation` | `'strict' \| 'format'` | `'strict'`: Full state match (recommended). `'format'`: Only validates state format |
| `minProviders` | `number` | Minimum providers that must be linked before a JWT is minted (default `1` when `providers` are configured) |
| `requiredProviders` | `string[]` | Provider ids that must all be linked before a JWT is minted |

### OAuth Endpoints

Local and remote modes expose standard OAuth endpoints:

| Endpoint | Method | Description |
| - | - | - |
| `/oauth/authorize` | GET | Start authorization flow |
| `/oauth/token` | POST | Exchange code for tokens |
| `/oauth/register` | POST | Dynamic Client Registration |
| `/oauth/userinfo` | GET | User profile information |
| `/.well-known/oauth-authorization-server` | GET | Server metadata |
| `/.well-known/jwks.json` | GET | Public signing keys |

### Use Cases

<CardGroup cols={2}>
  <Card title="Multi-Provider Federation" icon="object-group">
    Combine multiple IdPs under one session (Slack + GitHub + custom)
  </Card>

  <Card title="Progressive Authorization" icon="chart-line">
    Users authorize apps incrementally as needed
  </Card>

  <Card title="Full Token Control" icon="sliders">
    Custom token lifetimes, scopes, and refresh behavior
  </Card>

  <Card title="Tool-Authorization Enforcement" icon="list-check">
    Gate tools via the interactive consent picker (shipped) and enforce the selection at call time
  </Card>
</CardGroup>

<Warning>
  **Do NOT use `local` or `remote` mode when:**

  * You only have one IdP and don't need federation (use `transparent` instead)
  * You want to minimize auth complexity
  * Running multiple instances without Redis
</Warning>

***

## Discovery and Sessions by Mode

The protected resource metadata (`/.well-known/oauth-protected-resource`) advertises in `scopes_supported` the scopes a client can actually be given here:

| Mode | `scopes_supported` |
| - | - |
| `local`, `remote` | the literal entries of `allowedScopes` (a `*` glob names no scope and is left out) |
| `public` (or no `auth`) | `anonymousScopes`, what the anonymous grant gives |
| `static` | `scopes`, what the static credential carries |
| `transparent` | `requiredScopes`, then `scopes` (what the upstream provider is asked for) |

Outside local and remote mode, the scopes entries declare on their `authProviders` are listed too. A server that names no scope leaves `scopes_supported` out. The issuer named in discovery, on authorization responses and in tokens is the same for every request; see [The issuer](/frontmcp/authentication/local#the-issuer).

A session client (protocol revisions before 2026-07-28) resumes its session with the `mcp-session-id` header, or the `?sessionId=` query parameter on the legacy SSE `/message` endpoint. The server serves the request under that id only when `session:verify` verified it: in `public`, `static` and anonymous `transparent` mode the id must decrypt under `MCP_SESSION_SECRET` and carry the signature of the mode (in static mode, of the token that opened it); in the authenticated modes it must also belong to the caller's token. Any other id is answered with HTTP 404 (`-32000`), which tells the client to send `initialize` again; it is never used to look up a transport. An `initialize` that presents an unverified id starts a fresh session.

An MCP 2026-07-28 request has no session. An anonymous or static-key caller is identified for that request only (a fresh `anon:` subject, or `static:<digest>`), and no session is minted for it, so `MCP_SESSION_SECRET` is needed only for session clients (protocol revisions before 2026-07-28). In a tool, `this.context.sessionId` and `this.authInfo.sessionId` name the request, and `this.context.verifiedSessionId` is `undefined`: key anything that must outlive the request on the authenticated caller.

## Mode Selection Flowchart

```mermaid theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
graph TD
    A[Start] --> B{Need user identity?}
    B -->|No| C[Public Mode]
    B -->|Yes| D{Multiple providers?}
    D -->|No| E{Need progressive auth<br/>or consent UI?}
    D -->|Yes| F[Local/Remote Mode]
    E -->|No| G[Transparent Mode]
    E -->|Yes| F
    F --> H{Have upstream IdP?}
    H -->|Yes| I[Remote Mode]
    H -->|No| J[Local Mode]
```

***

## Security Comparison

| Security Aspect | Public | Transparent | Local | Remote |
| - | - | - | - | - |
| Token Verification | None | Against upstream JWKS | HS256 symmetric secret | HS256 symmetric secret (FrontMCP-issued session) |
| PKCE Support | N/A | Depends on IdP | Always S256 | Always S256 (downstream); upstream depends on IdP |
| Refresh Token Rotation | N/A | Depends on IdP | Always rotated | Depends on upstream IdP |
| Signing Key | `JWT_SECRET` (symmetric) | Upstream-managed | `JWT_SECRET` (symmetric) | `JWT_SECRET` (symmetric) + upstream JWKS |
| Tool-authz enforcement | No | No | Optional | Optional |
| Session Revocation | N/A | N/A | Supported | Supported |

***

## Token Verification Flow

```mermaid theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
sequenceDiagram
    participant Client
    participant FrontMCP
    participant JwksService

    Client->>FrontMCP: Request + Bearer token

    alt Public/Local/Remote Mode
        Note over FrontMCP: Verify HS256 signature<br/>with JWT_SECRET (symmetric)
        FrontMCP->>FrontMCP: Verify JWT locally
    else Transparent Mode
        FrontMCP->>JwksService: Get provider JWKS (cached)
        Note over JwksService: Fetch from jwksUri or<br/>discover via .well-known
        FrontMCP->>FrontMCP: Verify JWT against provider
    end

    alt Valid Token
        FrontMCP->>FrontMCP: Extract claims (sub, scopes, etc.)
        FrontMCP-->>Client: Authorized response
    else Invalid Token
        FrontMCP-->>Client: 401 Unauthorized<br/>WWW-Authenticate header
    end
```

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Remote OAuth" icon="cloud" href="/frontmcp/authentication/remote">
    Configure upstream IdP integration
  </Card>

  <Card title="Local OAuth" icon="server" href="/frontmcp/authentication/local">
    Set up self-contained authentication
  </Card>

  <Card title="Progressive Authorization" icon="forward" href="/frontmcp/authentication/progressive">
    Implement incremental app authorization
  </Card>

  <Card title="Production Deployment" icon="rocket" href="/frontmcp/authentication/production">
    Security checklist for production
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.