OIDC-federated user login

At a glance. Stack this on top of any other topology to change how users authenticate. Users click “Sign in with <provider>”, authenticate at your upstream IdP (Google Workspace / Okta / Entra / Auth0), and AuthPlane auto-provisions or links a local account. Token issuance still happens in AuthPlane — federation only changes login. One upstream IdP at a time; use Dex as a broker for multi-provider setups.

Topology

flowchart LR
    User["User<br/>browser"]
    IdP["Upstream IdP<br/>(Okta, Google, Entra)"]
    AS["AuthPlane"]
    Topo["Agent, MCP,<br/>Broker, ...<br/>(any topo.)"]

    User -->|login| IdP
    IdP -->|ID token| User
    AS -->|OAuth code exchange| IdP
    AS -->|"consent + token flow<br/>proceeds as normal"| Topo

Only login changes. Consent, token issuance, resource binding, scope enforcement — all identical to the non-federated version of your topology.

Flow

sequenceDiagram
    participant Agent
    participant AS as AuthPlane
    participant User
    participant Google

    Agent->>AS: /oauth/authorize?resource=…&scope=…
    AS->>User: Login page — clicks "Sign in with Google"
    User->>AS: GET /oidc/start?redirect=…
    AS->>Google: 302 to https://accounts.google.com/o/oauth2/v2/auth?…
    User->>Google: authenticates (MFA, SSO, whatever)
    Google->>AS: 302 to /oidc/callback?code=…
    AS->>Google: POST /token (exchange code for ID token)
    Google->>AS: ID token with email, name, sub, exp, ...
    AS->>AS: validates ID token (JWKS from Google's discovery)
    AS->>AS: looks up local user by (provider, provider_sub) — creates one<br/>on first sight using email + name from the ID token
    AS->>User: 302 to /consent (or /authorize continuation)
    AS->>AS: (rest of OAuth flow is standard)

Then: consent → auth code → /oauth/token → tokens. Every step from /oauth/authorize onward is identical to any other topology on this section.

When to use

  • Your org already uses Google Workspace / Okta / Entra ID / Auth0 / any OIDC provider.
  • You don’t want to manage passwords in AuthPlane.
  • You need your IdP’s MFA, conditional access, session policies, group memberships.
  • AuthPlane is being deployed for a team with existing corporate accounts.

Don’t use when:

  • Your users are external — federation to a corporate IdP won’t make sense.
  • You need multiple upstream IdPs simultaneously (Google + Okta at the same time) → use Dex as a broker in front of AuthPlane.
  • Users don’t have a browser (headless services) → XAA with jwt-bearer grant is the right move.

How to configure

Full walkthrough per provider in Guides: Federate to your IdP. Summary:

Register at your IdP:

  • App type: web application (OIDC).
  • Redirect URI: https://<your-authserver>/oidc/callback.
  • Note the client_id + client_secret + issuer URL.

AuthPlane config:

oidc:
  enabled: true
  issuer: https://accounts.google.com        # or your Okta / Entra / Auth0 issuer
  client_id: "your-client-id"
  client_secret: "your-client-secret"        # or use client_secret_ref (env-var name)
  display_name: Google                        # login button text
  redirect_uri: https://auth.example.com/oidc/callback
  scopes: [openid, email, profile]

Env-var equivalents: AUTHPLANE_OIDC_ENABLED, AUTHPLANE_OIDC_ISSUER, AUTHPLANE_OIDC_CLIENT_ID, AUTHPLANE_OIDC_CLIENT_SECRET, etc.

How AuthPlane handles it

  • /oidc/start — generates state + PKCE, sets HTTP-only session cookie, 302s to upstream /authorize.
  • /oidc/callback — validates state (HMAC-signed), exchanges code, validates the ID token against upstream JWKS (cached via xaa.jwks_cache_ttl).
  • UserAuthService — looks up the local user by the stable (provider, provider_sub) pair from the ID token. If present, that’s the account. If absent, auto-provisions a local user, using the ID token’s email and name claims to populate the new row (email is not the match key — a user can rename their email at Google and the same account will still resolve).
  • Session cookie is stamped; user is redirected back to /oauth/authorize — the OAuth flow that triggered login continues from where it left off.

oidc.show_local_login: false hides the password form entirely — everyone must authenticate through the IdP.

Verify

# Metadata check — /.well-known/openid-configuration doesn't announce OIDC (that's the IdP)
# but the login page shows the button
curl -s http://localhost:9000/login | grep -i "sign in with google"

# After login, user appears in the admin table
authserver admin user list
# → email=alice@example.com, provider="google", provider_sub="1234567890"

See also