Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7d34750
feat(oauth): add Enterprise-Managed Authorization (ID-JAG)
appleboy Jun 20, 2026
34561ce
fix(services): use collision-safe synthetic email for ID-JAG users
appleboy Jun 20, 2026
307a5e1
fix(services): block disabled users and scope escalation in ID-JAG
appleboy Jun 20, 2026
f89f43f
fix(services): authenticate confidential clients and enforce single I…
appleboy Jun 20, 2026
66acc8c
fix(services): enforce resource allowlist on mint and defer ID-JAG jt…
appleboy Jun 20, 2026
c46f0df
fix(services): honor revoked app authorization before minting ID-JAGs
appleboy Jun 21, 2026
acc6f21
fix(services): bind ID-JAG token-exchange to the user's granted consent
appleboy Jun 21, 2026
dff6b1f
fix(idjag): harden IdP minting, JWKS cache, and trusted-IdP scheme
appleboy Jun 21, 2026
ccd4ce9
fix(services): lock client type while admin-managed ID-JAG flow is en…
appleboy Jun 21, 2026
40e50ff
fix(idjag): widen synthetic ID-JAG identity suffix to 128 bits
appleboy Jun 21, 2026
da06e73
fix(jwks): preserve HTTPS scheme across redirects and enforce JWK key…
appleboy Jun 21, 2026
35e7814
fix(idjag): reject empty effective scope when minting ID-JAGs
appleboy Jun 21, 2026
119ba57
fix(idjag): bind ID-JAG resource to consent and honor JWK key_ops
appleboy Jun 21, 2026
83cc762
test(idjag): bind resource consent in token-exchange handler tests
appleboy Jun 21, 2026
b2c512d
fix(jwks): reject unsafe RSA public exponents from trusted IdP keys
appleboy Jun 21, 2026
8a93302
fix(idjag): bind minted scope to current client registration; reject …
appleboy Jun 21, 2026
3655691
fix(jwks): base no-kid acceptance on usable count; require RS256 RSA alg
appleboy Jun 21, 2026
f225c8f
fix(services): lock client scopes while admin-managed ID-JAG flow is …
appleboy Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -432,3 +432,43 @@ EXPIRED_TOKEN_CLEANUP_INTERVAL=1h # How often to run the cleanup (default:
# exact-only, so existing clients are unaffected. Set to false to instantly
# disable the feature without touching stored patterns.
# REDIRECT_URI_PATTERN_MATCHING_ENABLED=false

# ============================================================
# Enterprise-Managed Authorization (ID-JAG) — MCP extension
# ============================================================
# AuthGate can both ACCEPT and MINT Identity Assertion JWT Authorization Grants
# (ID-JAGs) so MCP clients obtain audience-bound access tokens for downstream MCP
# servers without per-server consent. Both roles are default-deny and OFF.
# See docs/ENTERPRISE_AUTH.md for the full flow and security model.

# --- RAS role: accept an ID-JAG via grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer ---
# Master switch (default false). When off, the jwt-bearer grant is refused and
# is not advertised in discovery metadata.
# IDJAG_ENABLED=false
#
# Trusted external IdPs whose ID-JAGs AuthGate will accept, as a JSON array.
# Each entry: issuer (exact `iss` match), jwks_uri (fetched, cached, refetched on
# key rotation), and audience (the value the assertion's `aud` must equal — i.e.
# this AuthGate as RAS; defaults to BASE_URL when omitted). Empty (the default)
# rejects every jwt-bearer request. Only RS256/ES256 IdPs are supported — HS256
# assertions are always rejected.
# TRUSTED_IDPS=[{"issuer":"https://idp.example.com","jwks_uri":"https://idp.example.com/.well-known/jwks.json","audience":"https://auth.example.com"}]
#
# JWKS fetch tuning (fail-closed: an IdP JWKS outage rejects the grant, never
# hangs the token endpoint).
# JWKS_CACHE_TTL=1h
# JWKS_FETCH_TIMEOUT=5s

# --- IdP role: mint an ID-JAG via grant_type=urn:ietf:params:oauth:grant-type:token-exchange ---
# Master switch (default false). When off, token-exchange is refused and not
# advertised in discovery metadata. NOTE: enabling this requires an asymmetric
# JWT_SIGNING_ALGORITHM (RS256/ES256) — startup fails under the default HS256,
# because a downstream RAS can only verify a minted ID-JAG via JWKS.
# IDJAG_ISSUANCE_ENABLED=false
#
# Allowlist of downstream RAS audiences AuthGate may mint ID-JAGs for
# (comma-separated). Empty (the default) rejects every token-exchange request.
# IDJAG_ALLOWED_AUDIENCES=https://auth.example.com,https://other-ras.example.com
#
# Lifetime of a minted ID-JAG (default 300s / 5m, matching the spec guidance).
# IDJAG_EXPIRATION=300s
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ In addition to Device Code Flow, AuthGate supports Authorization Code Flow with
- `OAuthApplication.AllowedResources` - Per-client RFC 8707 Resource Indicator allowlist (`StringArray`, `gorm:"type:json"`). A client may obtain a token whose `aud` was derived from a client-supplied `resource` only when the value is an **exact-string match** of an allowlist entry; enforced on all four grants (`client_credentials`, `authorization_code`, `device_code`, `refresh_token`) via the shared `services.validateClientResource` helper. **Deny-all by default**: an empty allowlist rejects any client-supplied `resource` with `invalid_target` (sending no `resource` still gets the operator-configured `JWT_AUDIENCE` fallback). Entries are syntax-validated at save time (same rules as `util.ValidateResourceIndicators`). Admins manage it in the client create/edit form. **Breaking change**: clients currently passing `resource` get `invalid_target` until an admin populates the allowlist. See `docs/MCP.md` "Per-client resource allowlist".
- `OAuthApplication.RedirectURIPatterns` - Per-client origin-locked path-prefix redirect patterns (`StringArray`, `gorm:"type:json"`). When the global `REDIRECT_URI_PATTERN_MATCHING_ENABLED` flag is on **and** the client has ≥1 pattern, a presented `redirect_uri` is also accepted if its scheme/host/port are **byte-exact** to a pattern and its path is the pattern path or a **sub-path** of it (so `…/callback` accepts `…/callback/ab12cd`, while `…/callbackXYZ`, a different origin, userinfo, `http://` downgrade, `..`-traversal, encoded slash, or any query is rejected). Matched in `services.matchRedirectPattern` (`internal/services/redirect_match.go`), gated in `AuthorizationService.isValidRedirectURI`. **Additive & default-deny**: empty list ⇒ today's exact matching, unchanged; flag off ⇒ patterns ignored (instant kill switch). The **token endpoint stays exact** — the authorization code binds the concrete `redirect_uri` and `ExchangeCode` still exact-matches it; pattern matching only widens `/oauth/authorize`. Save-time `services.validateRedirectURIPatterns` rejects bare-origin/`/`-only paths, wildcard/regex chars, query, fragment, userinfo, and applies the `STRICT_REDIRECT_URIS` loopback-only-`http` rule; capped at 10 patterns × 512 chars. Admins manage it in the client create/edit form. See `docs/CONFIGURATION.md` "Redirect URI pattern matching".
- `OAuthApplication.TokenProfile` - Per-client token lifetime preset: `short` (15min / 1d), `standard` (default, 10h / 30d), or `long` (24h / 90d). Preset TTLs are defined in `config.TokenProfiles` and overridable via `TOKEN_PROFILE_*` env vars; hard caps are enforced via `JWT_EXPIRATION_MAX` / `REFRESH_TOKEN_EXPIRATION_MAX`. Changes to a client's profile take effect on the next token issuance and refresh.
- `OAuthApplication.EnableIDJAGFlow` / `OAuthApplication.AllowedIDJAGIssuers` - Per-client toggle + trusted-IdP issuer allowlist for the MCP **Enterprise-Managed Authorization** (ID-JAG) flow. `EnableIDJAGFlow` is the per-client default-deny gate for **both** roles: redeeming an ID-JAG via `jwt-bearer` (RAS) **and** minting one via `token-exchange` (IdP) — enabling either master switch globally does not make a client eligible until an admin turns this on. Default-deny: flow off by default; an empty `AllowedIDJAGIssuers` accepts any `TRUSTED_IDPS` issuer, a non-empty one restricts to those `iss` values. **RAS role:** the assertion's `resource` is authorized against the existing `AllowedResources` allowlist, and the issued access token's `aud` is bound to it (access token only, no refresh); validation (signature via fetched IdP JWKS — RS256/ES256 only, `iss`/`aud`/`exp`, required claims, single-use `jti`) lives in `services/idjag_validator.go`; issuance + auto-provisioning in `services/token_jwt_bearer.go`. **IdP role:** minting via `grant_type=token-exchange` (`services/token_idjag_exchange.go`, `token/idjag.go`) additionally requires an active user **consent grant** for the client and binds the minted `scope`/`resource` to that grant (an omitted `scope` defaults to the granted scope, never the client's full registration). Both roles default-off behind `IDJAG_ENABLED` / `IDJAG_ISSUANCE_ENABLED`; trusted-IdP JWKS URIs must be `https` (cleartext `http` is allowed only for loopback). See `docs/ENTERPRISE_AUTH.md`.
- `UserAuthorization` - Per-app consent grants (one record per user+app pair)
- `AccessToken` - Unified storage for both access and refresh tokens (distinguished by `token_category` field)
- `User.IsActive` - Boolean (default `true`) controlling whether a user may log in. Disabling revokes all of the user's tokens and `RequireAuth` clears any live session on the next request. Guards prevent self-disable and disabling the last _active_ admin.
Expand Down Expand Up @@ -156,7 +157,7 @@ In addition to Device Code Flow, AuthGate supports Authorization Code Flow with
- Tracks authentication, device authorization, token operations, admin actions, security events
- Asynchronous batch writes (every 1s or 100 records) for minimal performance impact
- Automatic sensitive data masking (passwords, tokens, secrets)
- Event types: AUTHENTICATION*\*, DEVICE_CODE*\_, TOKEN\_\_, CLIENT\_\*, USER\_\* (`USER_CREATED`, `USER_DISABLED`, `USER_ENABLED`, `USER_PASSWORD_RESET`, `USER_ROLE_CHANGED`), `OAUTH_CONNECTION_DELETED`, `SESSION_REVOKED` / `SESSION_REVOKED_ALL` (browser login-session sign-out), RATE_LIMIT_EXCEEDED
- Event types: AUTHENTICATION*\*, DEVICE_CODE*\_, TOKEN\_\_, CLIENT\_\*, USER\_\* (`USER_CREATED`, `USER_DISABLED`, `USER_ENABLED`, `USER_PASSWORD_RESET`, `USER_ROLE_CHANGED`), `OAUTH_CONNECTION_DELETED`, `SESSION_REVOKED` / `SESSION_REVOKED_ALL` (browser login-session sign-out), `ID_JAG_TOKEN_ISSUED` (RAS jwt-bearer) / `ID_JAG_ISSUED` (IdP token-exchange), RATE_LIMIT_EXCEEDED
- Severity levels: INFO, WARNING, ERROR, CRITICAL
- Web interface at `/admin/audit` with filtering and CSV export

Expand Down Expand Up @@ -210,6 +211,8 @@ In addition to Device Code Flow, AuthGate supports Authorization Code Flow with
- `grant_type=urn:ietf:params:oauth:grant-type:device_code` - Exchange device_code for tokens
- `grant_type=authorization_code` - Exchange authorization code for tokens
- `grant_type=refresh_token` - Refresh access token
- `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer` - Exchange an ID-JAG (Enterprise-Managed Authorization, RAS role) for an audience-bound access token; gated by `IDJAG_ENABLED` + per-client `EnableIDJAGFlow`
- `grant_type=urn:ietf:params:oauth:grant-type:token-exchange` (`requested_token_type=...:token-type:id-jag`) - Mint an ID-JAG from an AuthGate ID Token (IdP role); gated by `IDJAG_ISSUANCE_ENABLED` + `IDJAG_ALLOWED_AUDIENCES`
- `GET /oauth/tokeninfo` - Verify JWT validity
- `POST /oauth/revoke` - Revoke tokens (RFC 7009)

Expand Down Expand Up @@ -294,6 +297,7 @@ Key configuration categories (see `.env.example` and `docs/CONFIGURATION.md` for
- `LOGIN_SESSION_TRACKING_ENABLED` (default `true`) - Server-side login-session registry powering the Active Sessions page / remote sign-out; the auth-path SID lookup is gated on this flag (kill switch). `LOGIN_SESSION_RETENTION_DAYS` (default `30`) - sweep window for revoked/expired login-session rows
- `CORS_ENABLED`, `CORS_ALLOWED_ORIGINS` - CORS for SPA frontends (applied to `/oauth/*` only)
- `REDIRECT_URI_PATTERN_MATCHING_ENABLED` (default `false`) - Global kill switch for origin-locked path-prefix `redirect_uri` matching. Off ⇒ exact matching only; on ⇒ clients with `RedirectURIPatterns` also accept origin-exact sub-path callbacks. Token-endpoint binding stays exact. See `OAuthApplication.RedirectURIPatterns` above and `docs/CONFIGURATION.md` "Redirect URI pattern matching"
- `IDJAG_ENABLED` (default `false`) + `TRUSTED_IDPS` (JSON array of `{issuer, jwks_uri, audience}`; empty = deny-all) + `JWKS_CACHE_TTL` (default `1h`) + `JWKS_FETCH_TIMEOUT` (default `5s`) - **Enterprise-Managed Authorization (ID-JAG), RAS role.** Accept an ID-JAG via `grant_type=jwt-bearer`; RS256/ES256 IdPs only, fail-closed JWKS fetch, single-use `jti`. A malformed `TRUSTED_IDPS` fails startup. `IDJAG_ISSUANCE_ENABLED` (default `false`) + `IDJAG_ALLOWED_AUDIENCES` (comma-separated; empty = deny-all) + `IDJAG_EXPIRATION` (default `300s`) - **IdP role**: mint ID-JAGs via `grant_type=token-exchange`. Both roles default-off; disabling a master switch instantly kills its grant and drops it from discovery. See `docs/ENTERPRISE_AUTH.md`

**OAuth Providers**

Expand Down
36 changes: 36 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,42 @@ Security model:
- **The token endpoint is unchanged.** The authorization code binds the *concrete* `redirect_uri` the browser actually used, and `POST /oauth/token` still requires an **exact** match against that bound value. Pattern matching only widens what `/oauth/authorize` accepts; it never weakens the code↔token binding.
- **No raw regex or wildcards.** Patterns are plain origin + path-prefix URIs. Save-time validation rejects bare-origin patterns (path must be more specific than `/`), wildcard/regex characters, queries, fragments, and userinfo, and applies the same loopback-only-`http` rule as `STRICT_REDIRECT_URIS`. A client may store at most 10 patterns, 512 characters each.

## Enterprise-Managed Authorization (ID-JAG)

AuthGate can accept and mint **Identity Assertion JWT Authorization Grants
(ID-JAGs)** for MCP's Enterprise-Managed Authorization extension. Both roles are
**off by default** and gated by their own master switch.

```bash
# RAS role — accept an ID-JAG (grant_type=...:grant-type:jwt-bearer)
IDJAG_ENABLED=false # master switch (default false)
TRUSTED_IDPS=[] # JSON array of {issuer, jwks_uri, audience}; empty rejects all
JWKS_CACHE_TTL=1h # cache lifetime for fetched IdP JWKS
JWKS_FETCH_TIMEOUT=5s # per-fetch timeout (fail-closed)

# IdP role — mint an ID-JAG (grant_type=...:grant-type:token-exchange)
IDJAG_ISSUANCE_ENABLED=false # master switch (default false)
IDJAG_ALLOWED_AUDIENCES= # comma-separated RAS audiences; empty rejects all
IDJAG_EXPIRATION=300s # minted ID-JAG lifetime
```

Key points:

- **Default-deny + kill switches.** Both master switches default off; an empty
`TRUSTED_IDPS` rejects every `jwt-bearer` request; an empty
`IDJAG_ALLOWED_AUDIENCES` rejects every `token-exchange` request. Per-client,
an admin must enable the flow and list the target `resource` in the client's
Allowed Resources allowlist.
- **Only RS256/ES256** external IdPs are validatable — HS256 assertions are
rejected. JWKS fetches are timeout-bounded, cached, and fail closed.
- A malformed `TRUSTED_IDPS` JSON value **fails startup** rather than silently
disabling the grant.
- Issued access tokens are **audience-bound to the assertion's `resource`**; no
refresh token is issued for `jwt-bearer`.

See **[ENTERPRISE_AUTH.md](ENTERPRISE_AUTH.md)** for the full flow, validation
rules, the discovery metadata it advertises, and the security model.

## TLS / HTTPS

AuthGate can serve HTTPS directly by setting two environment variables. When both are configured, the server listens on `SERVER_ADDR` using TLS. When both are empty (the default), it serves plain HTTP. Setting only one of the two is rejected at startup by `Config.Validate()` — this prevents silently falling back to HTTP when the operator meant to enable TLS.
Expand Down
Loading
Loading