Skip to content

feat: Add OAuth 2.0 Token Exchange (RFC 8693) support #112

@appleboy

Description

@appleboy

Summary

Add support for OAuth 2.0 Token Exchange (RFC 8693) as a new grant type. This enables trusted services (e.g., websites with existing LDAP/SSO authentication) to exchange a user identity assertion for an AuthGate access token — without requiring the user to log in to AuthGate separately.

Motivation

Organizations that already authenticate users via LDAP or other identity providers face a UX problem: if they also need AuthGate tokens for downstream services, users are forced to log in twice. Token Exchange solves this by allowing a trusted backend service to request tokens on behalf of an already-authenticated user.

Proposed Flow

User            Website (LDAP)           AuthGate
 │                   │                      │
 │── LDAP Login ────►│                      │
 │◄── Authenticated ─│                      │
 │                   │                      │
 │                   │── POST /oauth/token ────►│
 │                   │   grant_type=                │
 │                   │     urn:ietf:params:oauth:   │
 │                   │     grant-type:token-exchange │
 │                   │   subject_token=<JWT>        │
 │                   │   client_id + client_secret  │
 │                   │                      │
 │                   │                      │── Verify client identity ✓
 │                   │                      │── Verify subject_token signature ✓
 │                   │                      │── Lookup/create user ✓
 │                   │                      │── Issue access_token
 │                   │                      │
 │                   │◄── access_token ─────│
 │◄── Done ──────────│                      │

Request Format

POST /oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=eyJhbGciOiJSUzI1NiJ9...
&subject_token_type=urn:ietf:params:oauth:token-type:jwt
&scope=read write
Parameter Description
grant_type Fixed: urn:ietf:params:oauth:grant-type:token-exchange
subject_token JWT signed by the trusted service, representing user identity
subject_token_type urn:ietf:params:oauth:token-type:jwt
scope (Optional) Requested scopes, must be subset of client's allowed scopes
resource (Optional) Target resource URI
audience (Optional) Target service identifier

Subject Token (JWT from Trusted Service)

The trusted service signs a short-lived JWT with a pre-shared secret or registered public key:

{
  "iss": "https://your-website.com",
  "sub": "user123",
  "aud": "https://authgate.example.com",
  "exp": 1711000090,
  "iat": 1711000060,
  "email": "user@example.com",
  "name": "John Doe"
}

Key constraints:

  • Very short TTL (30–60 seconds) to prevent replay
  • aud must match AuthGate's base URL
  • iss must be a registered trusted issuer on the OAuth client

Response Format

{
  "access_token": "eyJhbGciOiJIUzI1NiJ9...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read write"
}

Validation Steps

  1. Verify client identityclient_id + client_secret (must be confidential client with EnableTokenExchange=true)
  2. Verify subject_token
    • Validate JWT signature against client's registered JWKS/shared secret
    • Check iss is in the client's trusted issuer whitelist
    • Check aud matches AuthGate's base URL
    • Check exp is not expired (enforce max TTL, e.g., 60s)
  3. Lookup or create user — Find user by sub claim; optionally auto-create if configured
  4. Validate scopes — Requested scopes must be subset of client's registered scopes
  5. Issue access token — Bound to the resolved user identity

Security Considerations

Layer Protection
Client authentication client_secret or HMAC ensures only trusted services can call
Subject token signature Prevents forging user identity (requires signing key)
Short-lived subject token 30–60s expiry minimizes replay window
Audience check Ensures token is intended for AuthGate, not another service
Issuer whitelist AuthGate only accepts tokens from registered issuers
Scope restriction Client can only request its allowed scopes
Audit logging Full trail of which client exchanged tokens for which user

Important limitation: Token Exchange prevents external/MITM attacks, but a trusted service holding the signing key can still impersonate any user. This is inherent to all delegation models. Mitigations include scope restrictions, audit logging, and short token lifetimes.

Implementation Plan

Model Changes

  • OAuthApplication: Add EnableTokenExchange bool field and TrustedIssuers (JSON array of allowed issuer URLs) and IssuerSecret or IssuerJWKS (for verifying subject tokens)

Handler Changes

  • internal/handlers/token.go: Add handleTokenExchangeGrant case in the token endpoint switch

Service Changes

  • internal/services/token.go: Add IssueTokenExchangeToken() method with full validation logic

Audit

  • Add TOKEN_EXCHANGE event type to audit logging

Configuration

  • No new global env vars required — configuration is per-client (on OAuthApplication)

Admin UI

  • Update client edit form to include Token Exchange toggle, trusted issuers, and issuer secret/JWKS fields

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions