You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The current OAuth implementation supports exactly one upstream Identity Provider at a time. All config fields (issuer, client_id, client_secret, auth_url, token_url, etc.) are single-valued. Organizations often need multiple IdPs simultaneously — e.g., Google for corporate users, GitHub for developers, and Keycloak for on-prem users.
Goal
Configure multiple IdPs. When a user hits /oauth/authorize, show a provider selection page listing all configured providers. After selection, continue the normal OAuth redirect flow with that provider's endpoints and credentials.
Current Architecture (Single IdP)
/oauth/authorize → redirect to single upstream auth_url → /oauth/callback → token exchange → mint gating token
Key single-IdP assumptions baked into the code:
Location
Single-IdP assumption
OAuthConfig (pkg/config/config.go:76-179)
All fields single-valued: Issuer, ClientID, ClientSecret, AuthURL, TokenURL, etc.
handleOAuthAuthorize (oauth_server.go:792-856)
Calls resolveUpstreamAuthURL() → returns one URL; uses one ClientID
handleOAuthCallback (oauth_server.go:858-995)
Uses single ClientID/ClientSecret for token exchange; validates against single issuer
Backward compatibility: If the legacy single-provider fields (issuer, client_id, etc.) are set and providers is empty, synthesize a single-entry providers array internally. Zero breaking changes.
Authorization Flow: Provider Selection Page
/oauth/authorize
↓
[provider param present?]
├── yes → store provider ID in pending state → redirect to that IdP
└── no → render provider selection HTML page
↓ (user clicks provider)
/oauth/authorize?...&provider=google
↓
store provider ID in pending state → redirect to Google
The selection page is a minimal, self-contained HTML page (no external dependencies) rendered by the server. Lists each configured provider with name and optional icon.
Callback: Provider-Aware Token Exchange
typeoauthPendingAuthstruct {
// ... existing fields ...ProviderIDstring// NEW: which IdP was chosen
}
In handleOAuthCallback:
Consume pending auth → get ProviderID
Look up provider config by ID → get that provider's TokenURL, ClientID, ClientSecret
Exchange code with correct provider endpoints
Validate identity token/userinfo against provider-specific issuer
Token Validation: Multi-Issuer
In gating mode, the MCP server is the token issuer (self-issued). No change needed — gating tokens are validated against gating_secret_key, not the upstream IdP.
In forward mode with multiple IdPs, validateOAuthClaims needs to accept any of the configured provider issuers (loop over providers[].issuer).
Metadata Endpoints
scopes_supported in authorization server metadata becomes the union of all providers' scopes.
Key Implementation Areas
New/modified files:
File
Changes
pkg/config/config.go
New OAuthProvider struct; Providers []OAuthProvider field; backward-compat migration of legacy fields
Embedded HTML (Go embed or inline string). Minimal design:
List of provider buttons (name + optional icon)
Each button links to /oauth/authorize?...&provider={id}
Preserves all original query params (client_id, redirect_uri, code_challenge, state, etc.)
No JavaScript required — pure HTML form/links
Dependencies
Add GitHub as OAuth Identity Provider #73 (GitHub IdP support) — the userinfo_claims_mapping, userinfo_email_url, and accept header config fields from that issue should be per-provider fields in the OAuthProvider struct rather than top-level OAuthConfig fields.
Open Questions
Should identity policy (allowed_email_domains, require_email_verified) be configurable per-provider, or always global?
Should mode (forward/gating) be per-provider or global? (Per-provider seems over-complex initially.)
Should the provider selection page be customizable (logo, colors, custom CSS)?
How should the provider ID be tracked for MCP protocol clients that may not render HTML? (e.g., pass provider as a query param in the initial authorize request)
Problem
The current OAuth implementation supports exactly one upstream Identity Provider at a time. All config fields (
issuer,client_id,client_secret,auth_url,token_url, etc.) are single-valued. Organizations often need multiple IdPs simultaneously — e.g., Google for corporate users, GitHub for developers, and Keycloak for on-prem users.Goal
Configure multiple IdPs. When a user hits
/oauth/authorize, show a provider selection page listing all configured providers. After selection, continue the normal OAuth redirect flow with that provider's endpoints and credentials.Current Architecture (Single IdP)
Key single-IdP assumptions baked into the code:
OAuthConfig(pkg/config/config.go:76-179)Issuer,ClientID,ClientSecret,AuthURL,TokenURL, etc.handleOAuthAuthorize(oauth_server.go:792-856)resolveUpstreamAuthURL()→ returns one URL; uses oneClientIDhandleOAuthCallback(oauth_server.go:858-995)ClientID/ClientSecretfor token exchange; validates against single issueroauthPendingAuthstruct (oauth_server.go:48-56)resolveUpstreamAuthURL/TokenURL(oauth_server.go:623-651)validateOAuthClaims(server.go:453-509)IssuerandAudienceoauth_server.go:653-714)mintGatingTokenResponse(oauth_server.go:1008-1069)Proposed Design
Config: Provider Array
Backward compatibility: If the legacy single-provider fields (
issuer,client_id, etc.) are set andprovidersis empty, synthesize a single-entryprovidersarray internally. Zero breaking changes.Authorization Flow: Provider Selection Page
The selection page is a minimal, self-contained HTML page (no external dependencies) rendered by the server. Lists each configured provider with name and optional icon.
Callback: Provider-Aware Token Exchange
In
handleOAuthCallback:ProviderIDTokenURL,ClientID,ClientSecretToken Validation: Multi-Issuer
In gating mode, the MCP server is the token issuer (self-issued). No change needed — gating tokens are validated against
gating_secret_key, not the upstream IdP.In forward mode with multiple IdPs,
validateOAuthClaimsneeds to accept any of the configured provider issuers (loop overproviders[].issuer).Metadata Endpoints
scopes_supportedin authorization server metadata becomes the union of all providers' scopes.Key Implementation Areas
New/modified files:
pkg/config/config.goOAuthProviderstruct;Providers []OAuthProviderfield; backward-compat migration of legacy fieldscmd/altinity-mcp/oauth_server.gopkg/server/server.gocmd/altinity-mcp/oauth_server_test.godocs/oauth_authorization.mdProvider selection page
Embedded HTML (Go
embedor inline string). Minimal design:/oauth/authorize?...&provider={id}client_id,redirect_uri,code_challenge,state, etc.)Dependencies
userinfo_claims_mapping,userinfo_email_url, and accept header config fields from that issue should be per-provider fields in theOAuthProviderstruct rather than top-levelOAuthConfigfields.Open Questions
allowed_email_domains,require_email_verified) be configurable per-provider, or always global?mode(forward/gating) be per-provider or global? (Per-provider seems over-complex initially.)provideras a query param in the initial authorize request)