oidc: add shared front-channel IdP logout plumbing#23739
Conversation
Shared building blocks enabling OIDC RP-initiated (front-channel) logout so that `consul logout` can terminate the identity provider session for tokens created via an OIDC auth method. The OIDC auth method type is Consul Enterprise only; the enterprise wiring lands separately in consul-enterprise. - internal/go-sso/oidcauth: add ClaimsFromAuthCodeWithIDToken (returns the raw id_token) and GetEndSessionEndpoint (reads end_session_endpoint from the cached discovery doc); add IDPLogout and OIDCPostLogoutRedirectURIs config. - api, agent/structs: add response-only ACLToken.IDPLogoutURL field that is never persisted to Raft and not part of the token content hash. - command/login: persist the provider logout URL to a sidecar next to the sink. - command/logout: open the provider end_session URL and remove the sidecar. Signed-off-by: Surabhi-1605 <surabhi.singh@hashicorp.com>
Go Test Coverage: 67.4%See the workflow run for the full per-package breakdown and downloadable HTML report. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #23739 +/- ##
==========================================
+ Coverage 59.72% 60.98% +1.26%
==========================================
Files 950 818 -132
Lines 114839 104929 -9910
==========================================
- Hits 68584 63993 -4591
+ Misses 39896 34808 -5088
+ Partials 6359 6128 -231 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds shared (CE-visible) plumbing for OIDC RP-initiated front-channel IdP logout, enabling consul logout to optionally trigger provider logout for OIDC-minted tokens by persisting a logout URL sidecar next to the token sink.
Changes:
- Extend OIDC auth-code claims exchange to optionally return the raw
id_token, and add a helper to readend_session_endpointfrom cached OIDC discovery. - Add response-only
IDPLogoutURLto ACL token representations (agent structs + public API model) for returning the provider logout URL on login. - Add CLI plumbing:
consul loginwrites a.oidc-logoutsidecar;consul logoutreads it, opens the URL in a browser, and deletes the sidecar.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/go-sso/oidcauth/oidc.go | Adds ClaimsFromAuthCodeWithIDToken, refactors common exchange logic, and adds GetEndSessionEndpoint() helper. |
| internal/go-sso/oidcauth/config.go | Adds OIDC config knobs for enabling IdP logout and post-logout redirect URI allowlisting. |
| command/login/login.go | Writes IdP logout URL to a .oidc-logout sidecar next to the token sink. |
| command/logout/logout.go | Best-effort reads sidecar and opens IdP logout URL in a browser; deletes the sidecar. |
| api/acl.go | Adds response-only ACLToken.IDPLogoutURL field to the public API model. |
| agent/structs/acl.go | Adds response-only structs.ACLToken.IDPLogoutURL with explicit non-persistence/non-hash contract. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // openURL opens the specified URL in the user's default browser. | ||
| func openURL(url string) error { | ||
| var name string | ||
| var args []string | ||
|
|
||
| switch runtime.GOOS { | ||
| case "windows": | ||
| name = "cmd.exe" | ||
| args = []string{"/c", "start"} | ||
| url = strings.ReplaceAll(url, "&", "^&") | ||
| case "darwin": | ||
| name = "open" | ||
| default: // "linux", "freebsd", "openbsd", "netbsd" | ||
| name = "xdg-open" | ||
| } | ||
| args = append(args, url) | ||
| return exec.Command(name, args...).Start() | ||
| } |
| // When the auth method has IdP (front-channel) logout enabled, the login | ||
| // response carries the provider's RP-initiated logout URL. Persist it next | ||
| // to the token sink so `consul logout` can terminate the IdP session. This | ||
| // is a no-op when the field is empty (e.g. IdP logout disabled or the auth | ||
| // method type does not support it). | ||
| c.writeLogoutSinkFromToken(tok) | ||
|
|
| // Best-effort IdP (front-channel) logout. If the matching `consul login` | ||
| // wrote an IdP logout sidecar file next to the token file, open the | ||
| // provider's RP-initiated logout URL in a browser to terminate the IdP | ||
| // session, then remove the sidecar. Absence of the sidecar preserves the | ||
| // existing behavior of only destroying the Consul token. | ||
| c.maybeIDPLogout() | ||
|
|
| // ClaimsFromAuthCodeWithIDToken behaves exactly like ClaimsFromAuthCode but | ||
| // additionally returns the raw OIDC id_token string obtained during the code | ||
| // exchange. Callers may persist this value to later supply it as the | ||
| // id_token_hint parameter for RP-initiated (front-channel) logout at the | ||
| // provider's end_session_endpoint. | ||
| // | ||
| // Requires the authenticator's config type be set to 'oidc'. | ||
| func (a *Authenticator) ClaimsFromAuthCodeWithIDToken(ctx context.Context, stateParam, code string) (*Claims, interface{}, string, error) { | ||
| return a.claimsFromAuthCode(ctx, stateParam, code) | ||
| } |
| // idpLogoutSuffix is appended to the token sink file path to derive the path of | ||
| // the companion file that stores the OIDC RP-initiated (front-channel) logout | ||
| // URL. `consul logout` looks for this file next to its token file. | ||
| const idpLogoutSuffix = ".oidc-logout" | ||
|
|
- oidcauth: clarify auth-code type-mismatch error (used by both entrypoints); return a descriptive error (not a nil cause) when the token exchange yields an invalid/expired token. - command/login: remove any stale .oidc-logout sidecar when the new token has no IdP logout URL, so a later logout never opens an outdated URL. - command/logout: harden the browser opener — reject non-http(s) URLs, use the Windows URL protocol handler (rundll32) instead of cmd.exe to avoid command injection, and add WSL handling; make openURL a package var for testability. - tests: add coverage for ClaimsFromAuthCodeWithIDToken (unredacted id_token), GetEndSessionEndpoint (discovery parsing; test server now advertises end_session_endpoint), the login sidecar write/remove, and the logout sidecar read/delete + URL validation. Signed-off-by: Surabhi-1605 <surabhi.singh@hashicorp.com>
Description
Adds the shared building blocks for OIDC RP-initiated (front-channel) logout so that
consul logoutcan terminate the identity provider (IdP) session for tokens that were created via an OIDC auth method. Todayconsul logoutonly deletes the Consul ACL token; the upstream IdP session is left alive.The OIDC auth method type is Consul Enterprise only, so this PR contains only the shared, CE-visible plumbing. The enterprise wiring (the
oidcauth method callback that builds the logout URL) lands separately inconsul-enterprise.What changed
internal/go-sso/oidcauthClaimsFromAuthCodeWithIDToken(...)— additive variant ofClaimsFromAuthCodethat also returns the rawid_token(needed asid_token_hint).ClaimsFromAuthCodeis preserved as a thin wrapper, so existing callers are unaffected.GetEndSessionEndpoint()— readsend_session_endpointfrom the provider's cached OIDC discovery document (no extra network round-trip).IDPLogout(bool, default off) andOIDCPostLogoutRedirectURIs.api/agent/structs— new response-onlyACLToken.IDPLogoutURLfield. It is never persisted to the state store / Raft and is not part of the token contentHash; it is only set on the login response.command/login— persists the provider logout URL to a sidecar file (<token-sink>.oidc-logout) alongside the token sink.command/logout— if that sidecar exists, opens the providerend_session_endpointin a browser and removes the sidecar. Absence of the sidecar preserves today's behavior exactly.Design notes
IDPLogout: true), default off — no behavior change for existing setups.id_tokenis returned to the client only in the login response and is never stored server-side, avoiding sensitive material in Raft.Testing
go build+go vetclean (main module andapi/submodule).internal/go-sso/oidcauth,command/login.id_token_hint;consul logoutterminates the IdP session (active sessions 1 → 0).Follow-ups (tracked separately)
oidcauth method callback (consul-enterprise).PR Checklist