Skip to content

oidc: add shared front-channel IdP logout plumbing#23739

Open
Surabhi-1605 wants to merge 5 commits into
mainfrom
feature/oidc-idp-logout-csl-15812
Open

oidc: add shared front-channel IdP logout plumbing#23739
Surabhi-1605 wants to merge 5 commits into
mainfrom
feature/oidc-idp-logout-csl-15812

Conversation

@Surabhi-1605

Copy link
Copy Markdown
Contributor

Description

Adds the shared building blocks for OIDC RP-initiated (front-channel) logout so that consul logout can terminate the identity provider (IdP) session for tokens that were created via an OIDC auth method. Today consul logout only 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 oidc auth method callback that builds the logout URL) lands separately in consul-enterprise.

What changed

  • internal/go-sso/oidcauth
    • ClaimsFromAuthCodeWithIDToken(...) — additive variant of ClaimsFromAuthCode that also returns the raw id_token (needed as id_token_hint). ClaimsFromAuthCode is preserved as a thin wrapper, so existing callers are unaffected.
    • GetEndSessionEndpoint() — reads end_session_endpoint from the provider's cached OIDC discovery document (no extra network round-trip).
    • New config fields IDPLogout (bool, default off) and OIDCPostLogoutRedirectURIs.
  • api / agent/structs — new response-only ACLToken.IDPLogoutURL field. It is never persisted to the state store / Raft and is not part of the token content Hash; 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 provider end_session_endpoint in a browser and removes the sidecar. Absence of the sidecar preserves today's behavior exactly.

Design notes

  • Opt-in via the auth method config (IDPLogout: true), default off — no behavior change for existing setups.
  • The id_token is returned to the client only in the login response and is never stored server-side, avoiding sensitive material in Raft.
  • Front-channel (browser redirect) chosen over back-channel per the RFC's RP-initiated logout model.

Testing

  • go build + go vet clean (main module and api/ submodule).
  • Unit tests pass: internal/go-sso/oidcauth, command/login.
  • Verified end-to-end against Keycloak (Enterprise binary): login writes the sidecar with a real id_token_hint; consul logout terminates the IdP session (active sessions 1 → 0).

Follow-ups (tracked separately)

  • Enterprise PR wiring the oidc auth method callback (consul-enterprise).
  • UI front-channel logout (redirect the browser on logout from the web UI).
  • Additional unit/integration test coverage for the new helpers.

PR Checklist

  • updated test coverage (follow-up)
  • external facing docs updated (n/a — Enterprise feature, docs land with ENT)
  • appropriate backport labels added
  • not creating new public API methods that are CE-only plumbing

Draft: shared plumbing only. Depends on the companion consul-enterprise PR to expose the feature.

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>
@github-actions github-actions Bot added theme/api Relating to the HTTP API interface theme/cli Flags and documentation for the CLI interface labels Jul 16, 2026
@Surabhi-1605 Surabhi-1605 added the pr/no-changelog PR does not need a corresponding .changelog entry label Jul 16, 2026
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Go Test Coverage: 67.4%

See the workflow run for the full per-package breakdown and downloadable HTML report.

@codecov-commenter

codecov-commenter commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 52.23881% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.98%. Comparing base (afbfd4a) to head (5994a14).

Files with missing lines Patch % Lines
command/logout/logout.go 47.50% 19 Missing and 2 partials ⚠️
internal/go-sso/oidcauth/oidc.go 59.25% 8 Missing and 3 partials ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Surabhi-1605
Surabhi-1605 marked this pull request as ready for review July 16, 2026 13:22
@Surabhi-1605
Surabhi-1605 requested review from a team as code owners July 16, 2026 13:22
@Surabhi-1605
Surabhi-1605 requested a review from Copilot July 16, 2026 13:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 read end_session_endpoint from cached OIDC discovery.
  • Add response-only IDPLogoutURL to ACL token representations (agent structs + public API model) for returning the provider logout URL on login.
  • Add CLI plumbing: consul login writes a .oidc-logout sidecar; consul logout reads 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.

Comment thread command/login/login.go
Comment thread command/logout/logout.go Outdated
Comment on lines +101 to +118
// 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()
}
Comment thread internal/go-sso/oidcauth/oidc.go
Comment thread command/login/login.go
Comment on lines +159 to +165
// 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)

Comment thread command/logout/logout.go
Comment on lines +60 to +66
// 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()

Comment on lines +77 to +86
// 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)
}
Comment thread command/login/login.go
Comment on lines +174 to +178
// 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr/no-backport pr/no-changelog PR does not need a corresponding .changelog entry theme/api Relating to the HTTP API interface theme/cli Flags and documentation for the CLI interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants