Skip to main content
Some identity providers don’t support Dynamic Client Registration (DCR). FrontMCP’s OAuth proxy bridges this gap by acting as a local OAuth server while delegating user authentication to the upstream IdP.

When to Use OAuth Proxy

Use Proxy When

  • IdP doesn’t support DCR
  • Need custom token claims
  • Want unified auth endpoints
  • Require consent UI layer

Use Direct When

  • IdP supports DCR (Auth0, Okta)
  • Simple pass-through needed
  • No custom claims required
  • Direct transparent mode works

How It Works

The proxy maintains:
  • Client registration with pre-provisioned credentials
  • JWKS for signing FrontMCP tokens
  • Session state linking upstream tokens to local sessions
  • Token vault storing upstream tokens for API calls

Configuration

Basic Setup

@FrontMcp({
  info: { name: 'MyServer', version: '1.0.0' },
  auth: {
    mode: 'orchestrated',
    type: 'remote',
    remote: {
      provider: 'https://legacy-idp.example.com',
      clientId: 'pre-provisioned-client-id',
      clientSecret: process.env.IDP_CLIENT_SECRET,
      scopes: ['openid', 'profile', 'email'],
    },
    consent: { enabled: true },
    sessionMode: 'stateful',
  },
})
export class Server {}

Configuration Options

OptionTypeDefaultDescription
remote.providerstringRequiredUpstream IdP base URL
remote.clientIdstringRequiredPre-registered client ID
remote.clientSecretstring-Client secret (confidential clients)
remote.scopesstring[]['openid']Scopes to request from IdP
consentConsentConfig{ enabled: false }Consent UI configuration
sessionMode'stateful' | 'stateless''stateful'Session strategy

Endpoint Overrides

For non-standard IdPs, override auto-discovered endpoints:
auth: {
  mode: 'orchestrated',
  type: 'remote',
  remote: {
    provider: 'https://legacy-idp.example.com',
    clientId: 'my-client',
    clientSecret: process.env.SECRET,

    // Override endpoints
    authEndpoint: 'https://legacy-idp.example.com/authorize',
    tokenEndpoint: 'https://legacy-idp.example.com/token',
    userInfoEndpoint: 'https://legacy-idp.example.com/userinfo',
    jwksUri: 'https://legacy-idp.example.com/keys',
  },
}

Inline JWKS

For IdPs without a JWKS endpoint:
remote: {
  provider: 'https://legacy-idp.example.com',
  jwks: {
    keys: [
      {
        kty: 'RSA',
        kid: 'key-1',
        alg: 'RS256',
        n: '...',
        e: 'AQAB',
      },
    ],
  },
}

Token Management

Upstream Token Storage

The proxy stores upstream IdP tokens in the token vault:
// Token vault structure (server-side)
{
  session_id: 'sess_abc123',
  frontmcp_tokens: {
    access_token: 'eyJ...',  // FrontMCP-issued
    refresh_token: 'frt_...',
  },
  upstream_tokens: {
    access_token: 'idp_...',  // From legacy IdP
    refresh_token: 'idp_rt_...',
    expires_at: 1234567890,
  },
}

Automatic Refresh

When upstream tokens expire, the proxy handles refresh:
auth: {
  mode: 'orchestrated',
  type: 'remote',
  remote: {
    provider: 'https://legacy-idp.example.com',
    clientId: 'my-client',
    refreshUpstream: true, // Auto-refresh upstream tokens
  },
  refresh: {
    accessTokenTtl: 3600,
    refreshTokenTtl: 604800,
  },
}

Custom Claims

Add claims from upstream user info to FrontMCP tokens:
auth: {
  mode: 'orchestrated',
  type: 'remote',
  remote: {
    provider: 'https://legacy-idp.example.com',
    clientId: 'my-client',
  },
  tokenClaims: async (upstreamUser) => ({
    // Map upstream claims to FrontMCP token
    department: upstreamUser.department,
    employee_id: upstreamUser.employee_number,
    roles: upstreamUser.groups?.map(g => g.name) ?? [],
  }),
}

Multi-Provider Proxy

Proxy multiple IdPs under one FrontMCP instance:
@App({
  name: 'CRM',
  auth: {
    mode: 'orchestrated',
    type: 'remote',
    remote: {
      provider: 'https://crm-idp.example.com',
      clientId: process.env.CRM_CLIENT_ID,
      clientSecret: process.env.CRM_CLIENT_SECRET,
    },
  },
})
export class CrmApp {}

@App({
  name: 'ERP',
  auth: {
    mode: 'orchestrated',
    type: 'remote',
    remote: {
      provider: 'https://erp-idp.example.com',
      clientId: process.env.ERP_CLIENT_ID,
      clientSecret: process.env.ERP_CLIENT_SECRET,
    },
  },
})
export class ErpApp {}

@FrontMcp({
  info: { name: 'EnterpriseSuite', version: '1.0.0' },
  apps: [CrmApp, ErpApp],
  auth: {
    mode: 'orchestrated',
    type: 'local',
    consent: { enabled: true },
    sessionMode: 'stateful',
  },
})
export class Server {}
Each app maintains its own upstream token in the vault. Users authenticate once per app via progressive authorization.

Proxy vs Direct Comparison

AspectOAuth ProxyDirect (Transparent)
Token IssuerFrontMCPUpstream IdP
JWKSFrontMCP-managedIdP-managed
Session ControlFullPass-through
Custom ClaimsYesNo
Consent UIOptionalNo
DCR RequiredNoYes (or pre-registered)
ComplexityHigherLower

Security Considerations

Store client secrets securely. Never commit secrets to version control. Use environment variables or a secrets manager.
Validate redirect URIs - Ensure callback URLs are registered with the upstream IdP
Use HTTPS - All communication with upstream IdP must be encrypted
Rotate secrets - Periodically rotate client secrets
Monitor token usage - Log and alert on unusual token patterns

Troubleshooting

  • Ensure session storage is configured (Redis for multi-instance)
  • Check that pending authorization TTL hasn’t expired
  • Verify the state parameter is being preserved
  • Verify client credentials are correct
  • Check that redirect URI matches exactly (including trailing slash)
  • Ensure scopes requested are allowed by IdP configuration
  • Confirm openid and profile scopes are requested
  • Check userinfo endpoint is accessible
  • Verify IdP returns expected claims
  • Some IdPs require offline_access scope for refresh tokens
  • Check if IdP rotates refresh tokens (handle rotation)
  • Verify refresh token hasn’t been revoked

Next Steps