EDFI-2772 Generalize OIDC logout - #211
Conversation
There was a problem hiding this comment.
Pull request overview
Generalizes Admin App logout to support standard OIDC RP-Initiated Logout across any configured OIDC provider by tracking the login provider in the session and building the IdP end-session redirect URL from discovery metadata, with a local-only fallback when an IdP doesn’t advertise an end_session_endpoint.
Changes:
- Registers OIDC providers on Nest bootstrap (
onModuleInit), keeps discovered clients, and exposesgetEndSessionUrl/getSoleOidcId. - Captures
oidcIdandid_tokenat login and uses them at logout to perform RP-Initiated Logout when supported (otherwise local-only logout + message). - Adds/updates unit tests and documentation to reflect the new generalized logout behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/fe/src/app/Layout/AppBar.tsx | Updates logout UI comment to be IdP-agnostic. |
| packages/api/src/auth/login/README.md | Documents RP-Initiated Logout + local-only fallback behavior. |
| packages/api/src/auth/login/oidc.strategy.ts | Moves provider registration to onModuleInit, stores clients, adds end-session URL builder and session typing for oidcId/idToken. |
| packages/api/src/auth/login/oidc.strategy.spec.ts | Adds tests for multi-provider registration, end-session URL building, and discovery failure behavior. |
| packages/api/src/auth/auth.controller.ts | Stores oidcId/idToken on session after login; rewrites logout to use discovered end-session URL and fallback behavior. |
| packages/api/src/auth/auth.controller.spec.ts | Adds tests for logout behavior and session tracking during login callback. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
328eafa to
ffcefc6
Compare
ffcefc6 to
1438e72
Compare
1438e72 to
d694aa6
Compare
Store oidcId and idToken on the session after login (after req.logIn, which regenerates the session) and build the logout URL from the provider's discovered end_session_endpoint with id_token_hint, replacing the hardcoded Keycloak path. Providers without an end_session_endpoint fall back to a local-only logout. The local-only logout message wording is a proposed candidate and still requires UX sign-off before release.
Use a type-only import for express-session so the SessionData type augmentation no longer emits a runtime require. Catch per-provider registration errors at the call site so one unreachable IdP can't reject the whole batch and block app bootstrap, logging the issuer when an unexpected failure occurs.
d694aa6 to
a5970ae
Compare
PR #211 Review — EDFI-2772 Generalize OIDC logout
Note Copilot's review comments have been lightly edited by Stephen, including tweaks to the severity. 🔴 CriticalNone found. 🟠 High
🟡 Medium
🔵 Low
Specialist Verdicts
SummaryThe core design (provider-aware RP-Initiated Logout via discovery metadata) is sound and well-tested at the controller level, and Keycloak/Google behavior was manually verified. The most important pre-merge fix is #5 (sole-provider fallback conflates "registered" with "configured," which can misdirect legacy-session logout), paired with #2 (silent session-save failures) since together they make provider-tracking failures invisible. The Generated by an automated 5-specialist team review (Security, Functionality, Maintainability, Usability, Test Coverage). |
Summary
Generalizes Admin App logout to the standard OpenID Connect RP-Initiated Logout flow for any configured OIDC provider, replacing the Keycloak-specific implementation. Logout selects the provider the user logged in with, builds the end-session URL from the provider's discovered
end_session_endpoint(withid_token_hint), and degrades to a local-only logout for providers that don't expose one. Keycloak keeps working with no regression.getEndSessionUrl, captures theid_tokenat login and registers providers inonModuleInit.oidcId/idTokenon the session at login and rewrites logout to build the end-session URL from discovery (full RP-Initiated Logout), with a local-only fallback for providers without anend_session_endpoint(Google).If you want to test these changes locally, you can follow this internal guide to switch providers in a dev environment:
Switching OIDC Providers on a Local Instance
Ticket
EDFI-2772 — Generalize logout to support any OIDC provider
Type of Change
What Changed
oidcIdandidToken(used asid_token_hintat logout).getEndSessionUrluses the discoveredend_session_endpointplusid_token_hint,post_logout_redirect_uri, andclient_id. RemovedconstructKeycloakLogoutUrland theSAMPLE_OIDC_CONFIG-based selection.end_session_endpoint(e.g. Google), logout destroys the local session and redirects to/unauthenticated?msg=…instead of a broken IdP redirect.onModuleInit(see Architectural Decisions).oidcId; when exactly one provider is registered, logout falls back to it so those legacy sessions still get a full IdP logout.Architectural Decisions
1. OIDC registration moved from the constructor to
onModuleInit. Discovery previously ran as a fire-and-forget promise in the constructor, which Nest doesn't await — a login arriving in that window failed with "Unknown authentication strategy" (startup race).onModuleInitis awaited during bootstrap, so the app serves requests only after all providers register. Trade-off: ready-time now includes one discovery round-trip per provider. Safe: each provider registers in its owntry/catch, so an unreachable IdP is skipped (logged, non-fatal) while the others register.2.
id_tokenretained server-side in the session as theid_token_hintfor logout; never exposed to the browser.3.
post_logout_redirect_uribuilt fromMY_URL_API_PATH— the same base used for the OIDCredirect_uris, so it matches what an OIDC client is configured with per the standard Admin App OIDC docs; providers that validate it accept it with no extra registration.Testing
Manual (E2E, all three providers, DevTools Network):
end_session_endpoint; no confirmation page; re-login required credentials (IdP session ended). PASS.…/oauth2/v2.0/logout; valid v2.0id_token(email claim present); re-login forced MFA (IdP session ended). Microsoft shows its own account-selection/confirmation screens (provider UX, not app-controlled). PASS.end_session_endpoint; local session destroyed, redirect to/unauthenticated?msg=…, no hop to Google. Silent re-login expected by design. PASS.Keycloak

Google WS

Entra ID
See comment on Jira's ticket (file too large to paste here)
Checklist
main)