Skip to content

feat(oidc): RP-Initiated Logout — backend issues end_session URL on logout #215

@an9xyz

Description

@an9xyz

Problem / Motivation

OIDC logout today only tears down the local Octo sessionPOST /v1/auth/oidc/<provider>/logout kicks all devices and revokes the locally-stored refresh token (modules/oidc/api.go). It does not end the IdP (Aegis) browser SSO session.

In our architecture the code → token exchange happens server-side in callback; the raw id_token is verified and then discarded. The frontend only ever receives the Octo LoginRespJSON via the authcode polling channel — it never holds the OIDC id_token. Therefore the frontend cannot construct an RP-Initiated Logout (end_session) URL on its own, because id_token_hint is not available to it.

User-visible symptom: after "logging out", clicking the OIDC login button again silently re-authenticates the user into the same account, because the Aegis SSO session cookie is still alive. Both the Octo session and the IdP browser session must be terminated.

Proposed Solution

Make the backend the single owner of the end_session URL (frontend stays free of any IdP details):

  1. On successful callback, encrypt and store the id_token (Redis, keyed by uid, with TTL) using the existing encryptor.
  2. Extend the existing POST /v1/auth/oidc/<provider>/logout (kept backward compatible — kick + revoke unchanged):
    • look up the stored id_token,
    • build the end_session URL with id_token_hint + a server-configured post_logout_redirect_uri (a single hard-coded login page; not accepted from the request body),
    • return it as a new end_session_url field in the JSON response.
  3. If no id_token is available (non-OIDC login / expired / evicted), omit end_session_url so the frontend can gracefully degrade to local-only logout.

The frontend then performs a top-level redirect to end_session_url; after Aegis clears its SSO session and redirects back, the frontend clears local login state.

Alternatives Considered

  • Return id_token to the frontend and let it build the URL — rejected. The id_token carries PII (email / phone / name) and would land in browser storage. Keeping it server-side (consistent with how the refresh token is already encrypted at rest) is the production-grade choice.
  • Backend proxies/calls end_session server-to-server — rejected. Terminating the Aegis session depends on the browser sending the Aegis-domain session cookie; a top-level browser redirect is required, not an XHR/server call.

Affected Component

Backend (Server)

Technical Approach

  • modules/oidc/oidc_client.go: parse end_session_endpoint from discovery (provider.Claims(&v)) and expose EndSessionEndpoint(); add a config override env as a fallback when discovery omits it.
  • modules/oidc/api.go callback success path: encrypt rawID with the existing NewEncryptor(cfg.Provider.RefreshTokenEncryptionKey) and store in Redis (oidc:idtoken:<uid>, configurable TTL).
  • modules/oidc/api.go logout handler: keep Kick + RevokeRefreshByUID untouched (best-effort, still always 200); look up + decrypt id_token, build the URL, return { status: 200, end_session_url }; delete the stored token after use.
  • modules/oidc/config.go: add PostLogoutRedirectURI (single hard-coded value), optional EndSessionURL override, and IDTokenTTL. Env names follow the existing OCTO_OIDC_PROVIDER_* convention.
  • URL carries only id_token_hint + post_logout_redirect_uri (discovery does not advertise state).
  • Never log the id_token.
  • TDD: table-driven tests for URL building / escaping, the no-id_token degrade path, and backward compatibility (no request body → behaves exactly as today).

Additional Context

  • Aegis integration doc §2.7 (RP-Initiated Logout): GET {issuer}/end_session?id_token_hint=...&post_logout_redirect_uri=....
  • Discovery ({issuer}/.well-known/openid-configuration) verified to expose end_session_endpoint; no front-channel / back-channel logout is advertised, so RP-Initiated Logout is the only mechanism.
  • Ops prerequisite (to confirm with the Aegis admin before release): whether the post_logout_redirect_uri must be registered/allow-listed on the IdP side, and whether id_token_hint is mandatory.
  • Companion frontend issue: feat(login): complete OIDC logout via backend-issued end_session redirect octo-web#184.

Metadata

Metadata

Assignees

Labels

priority:P2Medium — next sprinttype:featureNew feature or enhancement

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions