Skip to content

feat(auth): end the platform session on logout (OIDC RP-initiated logout) [PLT-107228] - #636

Open
shivendra6720 wants to merge 3 commits into
mainfrom
feat/plt-107228-cloud-logout
Open

feat(auth): end the platform session on logout (OIDC RP-initiated logout) [PLT-107228]#636
shivendra6720 wants to merge 3 commits into
mainfrom
feat/plt-107228-cloud-logout

Conversation

@shivendra6720

@shivendra6720 shivendra6720 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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:

Option Type Behavior
endSession boolean Clears local auth state, then redirects the browser to identity_/connect/endsession, terminating the Automation Cloud session and invalidating the refresh token. The next sign-in prompts for credentials. Browser-only; default false (previous behavior, byte-for-byte).
postLogoutRedirectUri string Where the user lands after the session is terminated — pass your app's URL, e.g. (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.

openid is required for the cloud logout

Identity'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:

Without openid scope With openid scope
logout({ endSession: true }) local state cleared only; console.warn("Cloud logout skipped… add the 'openid' scope"); no navigation silent sign-out via id_token_hint
postLogoutRedirectUri n/a (no redirect happens) honored (exact match)

client_id is never sent — there is no fallback path. The ID token is captured from token responses (AuthToken.id_tokenTokenInfo.idTokenTokenManager.getIdToken()), persisted with the token across page reloads, and kept across refreshes.

openid is 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 hold openid and have no user session to end.

Identity dependency — shipped

PLT-108129: Identity previously dropped post_logout_redirect_uri for 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

// Local logout only (default — previous behavior)
sdk.logout();

// Also log out of Automation Cloud (redirects the browser; without a
// postLogoutRedirectUri the user lands on the Automation Cloud portal)
sdk.logout({ endSession: true });

// Log out of Automation Cloud and return the user to this app
sdk.logout({
  endSession: true,
  postLogoutRedirectUri: (window.location.origin + window.location.pathname).replace(/\/$/, '')
});

…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>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://UiPath.github.io/uipath-typescript/pr-preview/pr-636/

Built to branch gh-pages at 2026-08-09 14:11 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Comment thread src/core/auth/service.ts Outdated
* 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 {

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.

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.

Suggested change
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({…}).

Comment thread src/core/auth/service.ts Outdated
Comment on lines +391 to +392
if (this.config.clientId) {
queryParams.set('client_id', this.config.clientId);

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.

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:

Suggested change
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();

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.

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:

Suggested change
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('');
    });

@claude

claude Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review summary

New finding (1):

  • tests/unit/core/auth/service.test.ts line 96 — vi.unstubAllGlobals() is called inline in the test body. If the preceding expect fails, the window global stub leaks into other tests. Move to an afterEach block, matching the pattern used in service-end-session.test.ts in this same PR.

Two pre-existing unresolved threads remain open (underscore-naming on buildEndSessionUrl, client_id fallback logic) — the fixes appear to be in the current code but those threads have not been resolved yet.

@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

…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>
@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ No issues found. Checked for bugs and CLAUDE.md compliance.

@shivendra6720
shivendra6720 marked this pull request as ready for review August 9, 2026 14:28
@shivendra6720
shivendra6720 requested a review from a team August 9, 2026 14:28
@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ No issues found. Checked for bugs and CLAUDE.md compliance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant