feat(auth): end the platform session on logout (OIDC RP-initiated logout) [PLT-107228] - #636
feat(auth): end the platform session on logout (OIDC RP-initiated logout) [PLT-107228]#636shivendra6720 wants to merge 3 commits into
Conversation
…out) [PLT-107228] sdk.logout() previously only cleared local tokens: the Identity session cookie survived, so the next login silently re-authenticated without prompting — users could not actually sign out or switch accounts. logout(options?) now supports OIDC RP-initiated logout: - `endSession: true` clears local auth state and then redirects the browser to Identity's end-session endpoint, terminating the Automation Cloud session (and refresh token) so the next sign-in prompts again. `postLogoutRedirectUri` optionally returns the user to the app. - The id_token is captured from token responses (AuthToken.id_token, TokenInfo.idToken, TokenManager.getIdToken) and sent as id_token_hint — required for Identity to honor post_logout_redirect_uri without a confirmation prompt; falls back to client_id when absent. - Without options, behavior is unchanged (local-only logout), so existing callers are unaffected. Includes the design doc, docs/authentication.md updates, and unit tests for the end-session URL construction and id_token plumbing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
| * available so Identity can skip its confirmation prompt and validate | ||
| * `post_logout_redirect_uri`; otherwise falls back to `client_id`. | ||
| */ | ||
| private _buildEndSessionUrl(params: { idTokenHint?: string; postLogoutRedirectUri?: string }): string { |
There was a problem hiding this comment.
Naming convention violation: the method has both the private keyword and an underscore prefix. Per conventions.md: "Prefer private keyword over underscore prefix for private methods." Drop the leading underscore.
| private _buildEndSessionUrl(params: { idTokenHint?: string; postLogoutRedirectUri?: string }): string { | |
| private buildEndSessionUrl(params: { idTokenHint?: string; postLogoutRedirectUri?: string }): string { |
Also update the call site at line 285: this.buildEndSessionUrl({…}).
| if (this.config.clientId) { | ||
| queryParams.set('client_id', this.config.clientId); |
There was a problem hiding this comment.
The JSDoc immediately above (and the design doc §7.2) say client_id is sent only as a fallback when id_token_hint is unavailable — "otherwise falls back to client_id". The implementation unconditionally sends client_id whenever it exists, so when id_token_hint is present both parameters are sent simultaneously.
This diverges from the stated intent. If Identity validates that client_id matches the audience in id_token_hint, sending both could cause unexpected rejections. If the belt-and-suspenders approach is intentional, the JSDoc and design doc need to be updated to say so; otherwise make it a true fallback:
| if (this.config.clientId) { | |
| queryParams.set('client_id', this.config.clientId); | |
| if (params.idTokenHint) { | |
| queryParams.set('id_token_hint', params.idTokenHint); | |
| } else if (this.config.clientId) { | |
| queryParams.set('client_id', this.config.clientId); | |
| } |
Also worth adding a test case that verifies client_id is absent when id_token_hint is present (currently missing from service-end-session.test.ts).
Identity now honors post_logout_redirect_uri for External Application
clients (PLT-108129, deployed to alpha 2026-08-06), validating it by
exact string match against the app's registered redirect URIs. Building
on that:
- pass the caller-supplied postLogoutRedirectUri through on
logout({ endSession: true }); the value must match a registered
redirect URI exactly (a trailing-slash difference is a mismatch) or
Identity falls back to the Automation Cloud portal
- send exactly one client identifier on the end-session request:
id_token_hint when available, client_id only as a fallback —
RP-initiated logout requires Identity to reject mismatched pairs
- warn when endSession runs without an OIDC ID token (openid scope
missing): Identity shows a confirmation page and ignores the return
redirect, which is otherwise silent and confusing
- rename _buildEndSessionUrl -> buildEndSessionUrl per conventions
- wire the data-fabric sample's Sign out button to the full flow;
validated on localhost and appsdev.alpha.uipath.host (v1.0.5), with
Identity-side telemetry confirming both redirect URIs accepted
- design doc: all Identity dependencies resolved, alpha validation log
and the exact-match finding recorded
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| service.logout({ endSession: true }); | ||
|
|
||
| expect(windowStub.location.href).toBe(''); | ||
| vi.unstubAllGlobals(); |
There was a problem hiding this comment.
Per the testing conventions: "Reset mocks in afterEach." Having vi.unstubAllGlobals() inline in the test body means if the expect assertion above it fails, the window global stub leaks into subsequent tests in this file.
Compare: service-end-session.test.ts in this same PR correctly puts cleanup in afterEach. The describe('logout') block here should do the same:
| vi.unstubAllGlobals(); | |
| vi.unstubAllGlobals(); |
describe('logout', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('should not redirect to end-session outside the browser', () => {
const windowStub = { location: { href: '' } };
vi.stubGlobal('window', windowStub);
const service = createService(TEST_CONSTANTS.ORGANIZATION_ID);
service.logout({ endSession: true });
expect(windowStub.location.href).toBe('');
});
Review summaryNew finding (1):
Two pre-existing unresolved threads remain open (underscore-naming on |
|
…PLT-107228] Identity's end-session endpoint is unauthenticated — the id_token_hint is what proves the request (confirmed by the Identity team), so a client_id fallback can never complete a silent logout: - endSession now runs only when an OIDC ID token is available (openid scope). Without one the SDK clears local state, logs a "Cloud logout skipped" warning naming the missing scope, and does not navigate — no more half-working confirmation-page path. - client_id is never sent on the end-session request; the URL builder takes idTokenHint as required. - docs (LogoutOptions, UiPath.logout, authentication.md) stop framing postLogoutRedirectUri around the login redirect URI and instead show the app-URL expression used by the sample: (window.location.origin + window.location.pathname).replace(/\/$/, '') keeping only the exact-string-match / trailing-slash caveat. - remove the design doc (docs/design/plt-107228-cloud-logout.md). - restore the original _buildEndSessionUrl name. Unit tests reworked for the new contract: skip-without-token, never client_id, warning trio. 2131 unit tests pass; typecheck and lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
✅ No issues found. Checked for bugs and CLAUDE.md compliance. |
|
✅ No issues found. Checked for bugs and CLAUDE.md compliance. |



What this adds
sdk.logout()previously cleared only local state: the Automation Cloud (Identity) session survived, so the next sign-in silently re-authenticated — users could not actually sign out or switch accounts.logout(options?)now supports OIDC RP-initiated logout:endSessionbooleanidentity_/connect/endsession, terminating the Automation Cloud session and invalidating the refresh token. The next sign-in prompts for credentials. Browser-only; defaultfalse(previous behavior, byte-for-byte).postLogoutRedirectUristring(window.location.origin + window.location.pathname).replace(/\/$/, ''). Identity compares the value by exact string match (a trailing-slash difference is a mismatch); omitted or mismatched → the Automation Cloud portal.openidis required for the cloud logoutIdentity's end-session endpoint is unauthenticated — the OIDC ID token (
id_token_hint) is what proves the request (confirmed by the Identity team). The SDK therefore performs the end-session redirect only when an ID token is available:openidscopeopenidscopelogout({ endSession: true })console.warn("Cloud logout skipped… add the 'openid' scope"); no navigationid_token_hintpostLogoutRedirectUriclient_idis never sent — there is no fallback path. The ID token is captured from token responses (AuthToken.id_token→TokenInfo.idToken→TokenManager.getIdToken()), persisted with the token across page reloads, and kept across refreshes.openidis not appended to the scope automatically: a registration lacking it would break login outright at/authorize— an unacceptable trade for a logout feature. Consumers opt in; the warning makes the missing scope discoverable. Secret-based (confidential) auth skips end-session entirely — confidential apps cannot holdopenidand have no user session to end.Identity dependency — shipped
PLT-108129: Identity previously dropped
post_logout_redirect_urifor External Application clients and fell back to the portal. The fix (merged, deployed to alpha 2026-08-06) validates the passed URI and honors it.Example Usage