You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
OIDC logout today only tears down the local Octo session — POST /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):
On successful callback, encrypt and store the id_token (Redis, keyed by uid, with TTL) using the existing encryptor.
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-configuredpost_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.
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.gocallback success path: encrypt rawID with the existing NewEncryptor(cfg.Provider.RefreshTokenEncryptionKey) and store in Redis (oidc:idtoken:<uid>, configurable TTL).
modules/oidc/api.gologout 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.
Problem / Motivation
OIDC logout today only tears down the local Octo session —
POST /v1/auth/oidc/<provider>/logoutkicks 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 → tokenexchange happens server-side incallback; the rawid_tokenis verified and then discarded. The frontend only ever receives the OctoLoginRespJSONvia the authcode polling channel — it never holds the OIDCid_token. Therefore the frontend cannot construct an RP-Initiated Logout (end_session) URL on its own, becauseid_token_hintis 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_sessionURL (frontend stays free of any IdP details):callback, encrypt and store theid_token(Redis, keyed by uid, with TTL) using the existing encryptor.POST /v1/auth/oidc/<provider>/logout(kept backward compatible — kick + revoke unchanged):id_token,end_sessionURL withid_token_hint+ a server-configuredpost_logout_redirect_uri(a single hard-coded login page; not accepted from the request body),end_session_urlfield in the JSON response.id_tokenis available (non-OIDC login / expired / evicted), omitend_session_urlso 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
id_tokento the frontend and let it build the URL — rejected. Theid_tokencarries 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.end_sessionserver-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: parseend_session_endpointfrom discovery (provider.Claims(&v)) and exposeEndSessionEndpoint(); add a config override env as a fallback when discovery omits it.modules/oidc/api.gocallbacksuccess path: encryptrawIDwith the existingNewEncryptor(cfg.Provider.RefreshTokenEncryptionKey)and store in Redis (oidc:idtoken:<uid>, configurable TTL).modules/oidc/api.gologouthandler: keepKick+RevokeRefreshByUIDuntouched (best-effort, still always 200); look up + decryptid_token, build the URL, return{ status: 200, end_session_url }; delete the stored token after use.modules/oidc/config.go: addPostLogoutRedirectURI(single hard-coded value), optionalEndSessionURLoverride, andIDTokenTTL. Env names follow the existingOCTO_OIDC_PROVIDER_*convention.id_token_hint+post_logout_redirect_uri(discovery does not advertisestate).id_token.id_tokendegrade path, and backward compatibility (no request body → behaves exactly as today).Additional Context
GET {issuer}/end_session?id_token_hint=...&post_logout_redirect_uri=....{issuer}/.well-known/openid-configuration) verified to exposeend_session_endpoint; no front-channel / back-channel logout is advertised, so RP-Initiated Logout is the only mechanism.post_logout_redirect_urimust be registered/allow-listed on the IdP side, and whetherid_token_hintis mandatory.