Skip to main content
Progressive authorization allows users to authorize apps incrementally, rather than all at once. This improves UX by only requesting access when tools actually need it.

How It Works


Configuration

Enable progressive auth with orchestrated mode:
@FrontMcp({
  info: { name: 'MyServer', version: '1.0.0' },
  auth: {
    mode: 'orchestrated',
    type: 'local',
    consent: { enabled: true }, // Enable consent UI
    sessionMode: 'stateful', // Required for token vault
  },
  apps: [SlackApp, GitHubApp, CrmApp],
})
export class Server {}

Authorization Hierarchy

Progressive auth operates at three levels:

Token Vault

The token vault stores per-app credentials and expands as users authorize more apps:

Initial State

Session Token: user-123Vault:
  • CRM: Authorized

After Slack Auth

Session Token: user-123 (same)Vault:
  • CRM: Authorized
  • Slack: Authorized

After GitHub Auth

Session Token: user-123 (same)Vault:
  • CRM: Authorized
  • Slack: Authorized
  • GitHub: Authorized
The session token remains the same. Only the token vault expands with new app credentials.

Authorization Response

When a tool requires unauthorized access, FrontMCP returns:
{
  "error": "authorization_required",
  "code": "AUTH_REQUIRED",
  "app": "slack",
  "tool": "slack:send_message",
  "required_scopes": ["chat:write"],
  "auth_url": "https://my-server/oauth/authorize?app=slack&scope=chat:write",
  "message": "Slack authorization required",
  "hint": "Click the authorization link to grant access to Slack"
}

Handling in Clients

try {
  const result = await mcpClient.callTool('slack:send_message', { message: 'Hello' });
} catch (error) {
  if (error.code === 'AUTH_REQUIRED') {
    // Show auth link to user
    console.log(`Please authorize: ${error.auth_url}`);
  }
}

The built-in consent UI lets users choose which apps to authorize:
+----------------------------------------------------------+
|                    Authorize Access                        |
|                                                            |
|  MyApp requests access to the following services:          |
|                                                            |
|  +------------------------------------------------------+  |
|  |  CRM (Auth0)                           [Authorized]  |  |
|  |  Tools: get_contacts, update_contact                 |  |
|  +------------------------------------------------------+  |
|                                                            |
|  +------------------------------------------------------+  |
|  |  Slack                                    [Skipped]  |  |
|  |  Tools: send_message, list_channels                  |  |
|  |                                                      |  |
|  |  [ Authorize Later ]                                 |  |
|  +------------------------------------------------------+  |
|                                                            |
|  +------------------------------------------------------+  |
|  |  GitHub                                   [Pending]  |  |
|  |  Tools: create_issue, list_repos                     |  |
|  |                                                      |  |
|  |  [ Authorize ]  [ Skip ]                             |  |
|  +------------------------------------------------------+  |
|                                                            |
|            [ Continue with authorized apps ]               |
+----------------------------------------------------------+

Multi-Provider Setup

App Configuration

@App({
  name: 'Slack',
  auth: {
    mode: 'transparent',
    remote: {
      provider: 'https://slack.com/oauth',
      scopes: ['chat:write', 'channels:read'],
    },
  },
})
export class SlackApp {
  @Tool({ name: 'send_message' })
  async sendMessage(ctx: ToolContext, input: { message: string }) {
    // Uses Slack token from vault
  }
}

@App({
  name: 'GitHub',
  auth: {
    mode: 'transparent',
    remote: {
      provider: 'https://github.com/login/oauth',
      scopes: ['repo', 'user'],
    },
  },
})
export class GitHubApp {
  @Tool({ name: 'create_issue' })
  async createIssue(ctx: ToolContext, input: { title: string }) {
    // Uses GitHub token from vault
  }
}

Server Configuration

@FrontMcp({
  info: { name: 'AgentSuite', version: '1.0.0' },
  auth: {
    mode: 'orchestrated',
    type: 'local',
    consent: { enabled: true },
    sessionMode: 'stateful',
    tokenStorage: {
      type: 'redis',
      config: {
        host: process.env.REDIS_HOST!,
        port: parseInt(process.env.REDIS_PORT || '6379'),
      },
    },
  },
  apps: [SlackApp, GitHubApp],
})
export class Server {}

Standalone vs Nested Apps

Apps can be configured as standalone (direct access) or nested (under parent):
ConfigurationDirect AccessFederated Auth
standalone: true/slack/oauth/authorizeAlso in parent consent
standalone: false (default)N/AOnly via parent
@App({
  name: 'Slack',
  standalone: true, // Direct access at /slack
  auth: { /* ... */ },
})
export class SlackApp {}

Skip and Authorize Later

Users can skip apps during initial consent and authorize later:

Skipping

// User skips Slack during initial auth
// Session created with: authorized_apps: ['crm'], skipped_apps: ['slack']

Later Authorization

GET /oauth/authorize?app=slack&prompt=consent
This triggers a targeted authorization flow for just the skipped app.

Session Token Structure

{
  "sub": "user-123",
  "iss": "https://my-server",
  "iat": 1234567890,
  "exp": 1234571490,
  "session_id": "sess_abc123",
  "authorized_apps": ["crm", "billing"],
  "pending_apps": ["slack", "github"],
  "scopes": ["crm:read", "crm:write", "billing:read"]
}
Child tokens are stored in the Token Vault (server-side), not embedded in the JWT.

OpenAPI Adapter Integration

When using OpenAPI adapters, tools are automatically grouped by auth provider:
@FrontMcp({
  info: { name: 'APIGateway', version: '1.0.0' },
  auth: {
    mode: 'orchestrated',
    type: 'local',
    consent: { enabled: true },
  },
  adapters: [
    {
      type: 'openapi',
      spec: 'https://api.github.com/openapi.json',
      auth: {
        mode: 'transparent',
        remote: {
          provider: 'https://github.com',
          clientId: 'github-client-id',
        },
      },
    },
    {
      type: 'openapi',
      spec: 'https://api.stripe.com/openapi.json',
      auth: {
        mode: 'transparent',
        remote: {
          provider: 'https://connect.stripe.com',
          clientId: 'stripe-client-id',
        },
      },
    },
  ],
})
export class Server {}
Tools from each adapter are grouped by their auth configuration and appear in the consent UI accordingly.

Best Practices

Request minimal scopes - Only ask for what each tool needs
Provide clear descriptions - Users should understand why each app is needed
Handle auth errors gracefully - Show friendly messages with auth links
Use stateful sessions - Required for token vault to work
Test the skip flow - Ensure skipped apps can be authorized later

Troubleshooting

  • Ensure sessionMode: 'stateful' is configured
  • Check Redis connectivity if using Redis storage
  • Verify the session ID matches across requests
  • Use prompt=consent to force the consent screen
  • Check that the app wasn’t excluded via excludeFromParent

Next Steps