Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ useEffect(() => {
- `sdk.isInOAuthCallback()` - Check if processing OAuth redirect
- `sdk.completeOAuth()` - Manually complete OAuth (advanced use)
- `sdk.getToken()` - Get the logged-in user's access token
- `sdk.logout()` - Logout and clear all authentication state (requires re-initialization to authenticate again)
- `sdk.logout()` - Logout and clear all authentication state (requires re-initialization to authenticate again). By default this is local-only: the Automation Cloud session is untouched and a subsequent sign-in completes silently. Pass `sdk.logout({ endSession: true })` to also terminate the Automation Cloud session — the browser is redirected to the Identity end-session endpoint, so the next sign-in prompts for credentials. **The cloud logout requires the `openid` scope in your SDK configuration**: the SDK sends the OIDC ID token as `id_token_hint` to prove the end-session request; without an ID token the SDK clears local state only, logs a warning, and skips the cloud logout. Pass `postLogoutRedirectUri` to return the user to your app afterwards — e.g. `(window.location.origin + window.location.pathname).replace(/\/$/, '')`. Identity compares the value by exact string match (a trailing-slash difference is a mismatch); when omitted or mismatched, the user lands on the Automation Cloud portal.
- `sdk.updateToken()` - Inject a refreshed token into the SDK instance (useful for backend services managing token lifecycle)

---
Expand Down
24 changes: 23 additions & 1 deletion samples/data-fabric-app/src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,30 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
}

/**
* Signs the user out of the app AND Automation Cloud (OIDC RP-initiated
* logout): local tokens are cleared, then the browser is redirected to
* Identity's end-session endpoint and returned here via
* `postLogoutRedirectUri`. The next sign-in prompts for credentials.
* Requires the `openid` scope (see uipath.json) — without it, Identity
* shows a confirmation page and skips the return redirect (the SDK logs
* a warning).
*
* For an app-only logout — cloud session survives, next sign-in is
* silent — call `sdk.logout()` with no options instead.
*/
const logout = () => {
sdk.logout()
sdk.logout({
endSession: true,
// Identity validates this against the app's registered redirect URIs
// by EXACT string match, and registrations carry no trailing slash —
// on localhost, pathname is "/" and "http://localhost:5173/" would be
// silently rejected (landing the user on the portal). Strip it.
postLogoutRedirectUri: (
window.location.origin + window.location.pathname
).replace(/\/$/, ''),
})
// The redirect is asynchronous — clear UI state for the interim tick.
setIsAuthenticated(false)
setError(null)
}
Expand Down
49 changes: 46 additions & 3 deletions src/core/auth/service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Config } from '../config/config';
import { ExecutionContext } from '../context/execution';
import { TokenManager } from './token-manager';
import { AuthToken, TokenInfo, OAuthContext } from './types';
import { AuthToken, TokenInfo, OAuthContext, LogoutOptions } from './types';
import { AUTH_STORAGE_KEYS } from './constants';
import { hasOAuthConfig } from '../config/sdk-config';
import { isBrowser } from '../../utils/platform';
Expand Down Expand Up @@ -254,8 +254,29 @@ export class AuthService {

/**
* Clears all authentication state including tokens and stored OAuth context.
* With `endSession: true`, additionally redirects the browser to the
* Identity end-session endpoint so the Automation Cloud session (and
* refresh token) are terminated — local cleanup alone cannot reach them,
* and without this the next sign-in silently reuses the cloud session.
* The end-session redirect requires the OIDC ID token (the `openid`
* scope): it is sent as `id_token_hint` to prove the request. Without one
* the cloud logout is skipped — only local state is cleared — and a
* warning is logged.
*/
public logout(): void {
public logout(options?: LogoutOptions): void {
// Capture the ID token before clearToken() wipes it.
const idTokenHint = options?.endSession ? this.tokenManager.getIdToken() : undefined;

// End-session is an unauthenticated Identity endpoint — id_token_hint is
// what proves the request, so without `openid` there is nothing to send.
if (options?.endSession && isBrowser && !idTokenHint) {
console.warn(
'Cloud logout skipped: no OIDC ID token is available, so only local ' +
"authentication state was cleared. Add the 'openid' scope to your " +
'SDK configuration to enable endSession.'
);
}

this.tokenManager.clearToken();

// Clear OAuth context from session storage. These are normally cleaned up in _handleOAuthCallback after a successful
Expand All @@ -270,6 +291,13 @@ export class AuthService {
console.warn('Failed to clear OAuth context from session storage', error);
}
}

if (options?.endSession && isBrowser && idTokenHint) {
window.location.href = this._buildEndSessionUrl({
idTokenHint,
postLogoutRedirectUri: options.postLogoutRedirectUri
});
}
}

/**
Expand Down Expand Up @@ -360,6 +388,20 @@ export class AuthService {
: `${authorizeUrl}&acr_values=${acrValues}`;
}

/**
* Builds the Identity end-session URL used to terminate the Automation
* Cloud session (OIDC RP-initiated logout). `id_token_hint` proves the
* request and lets Identity validate `post_logout_redirect_uri`.
*/
private _buildEndSessionUrl(params: { idTokenHint: string; postLogoutRedirectUri?: string }): string {
const queryParams = new URLSearchParams();
queryParams.set('id_token_hint', params.idTokenHint);
if (params.postLogoutRedirectUri) {
queryParams.set('post_logout_redirect_uri', params.postLogoutRedirectUri);
}
return `${this.config.baseUrl}/${IDENTITY_ENDPOINTS.END_SESSION}?${queryParams.toString()}`;
}

/**
* Exchanges the authorization code for an access token and automatically updates the current token
*/
Expand Down Expand Up @@ -396,7 +438,8 @@ export class AuthService {
token: token.access_token,
type: 'oauth',
expiresAt: token.expires_in ? new Date(Date.now() + token.expires_in * 1000) : undefined,
refreshToken: token.refresh_token
refreshToken: token.refresh_token,
idToken: token.id_token
});

return token;
Expand Down
12 changes: 11 additions & 1 deletion src/core/auth/token-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,14 @@ export class TokenManager {
return this.currentToken?.token;
}

/**
* Gets the OIDC ID token from the current token, if present.
* Used as `id_token_hint` for RP-initiated logout.
*/
getIdToken(): string | undefined {
return this.currentToken?.idToken;
}

/**
* Checks if we have a valid token
*/
Expand Down Expand Up @@ -345,7 +353,9 @@ export class TokenManager {
token: token.access_token,
type: 'oauth',
expiresAt: new Date(Date.now() + token.expires_in * 1000),
refreshToken: token.refresh_token
refreshToken: token.refresh_token,
// A refresh response may omit id_token — keep the one from initial login.
idToken: token.id_token ?? tokenInfo.idToken
});
return token;
}
Expand Down
37 changes: 37 additions & 0 deletions src/core/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,42 @@ export interface TokenInfo {
type: 'secret' | 'oauth';
expiresAt?: Date;
refreshToken?: string;
/**
* OIDC ID token, present only when the `openid` scope was requested.
* Used as `id_token_hint` for RP-initiated logout.
*/
idToken?: string;
}

/**
* Options controlling logout behavior
*/
export interface LogoutOptions {
/**
* When true, after clearing local authentication state the browser is
* redirected to the Identity end-session endpoint, terminating the
* Automation Cloud session (and invalidating the refresh token) so the
* next sign-in prompts for credentials instead of silently reusing the
* still-active cloud session. Browser-only. Defaults to false, which
* preserves the previous local-only logout behavior.
*
* Requires the `openid` scope in your SDK configuration: the SDK captures
* the OIDC ID token and sends it as `id_token_hint`, which is what proves
* the end-session request. Without an ID token the cloud logout is
* skipped — only local state is cleared — and a warning is logged.
*/
endSession?: boolean;
/**
* URL the user is returned to after the cloud session is terminated — pass
* this to bring the user back to your app instead of the Automation Cloud
* portal, e.g.
* `(window.location.origin + window.location.pathname).replace(/\/$/, '')`.
*
* Identity compares the value by exact string match, so even a
* trailing-slash difference is a mismatch — when omitted or mismatched,
* the user lands on the Automation Cloud portal instead.
*/
postLogoutRedirectUri?: string;
}

/**
Expand All @@ -17,6 +53,7 @@ export interface AuthToken {
expires_in: number;
scope: string;
refresh_token?: string;
id_token?: string;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

export { UiPath } from './uipath';
export type { UiPathSDKConfig } from './config/sdk-config';
export type { TokenInfo } from './auth/types';
export type { TokenInfo, LogoutOptions } from './auth/types';
export * from './errors';

// Pagination (common across all services)
Expand Down
37 changes: 34 additions & 3 deletions src/core/uipath.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UiPathConfig } from './config/config';
import { ExecutionContext } from './context/execution';
import { AuthService } from './auth/service';
import { TokenInfo } from './auth/types';
import { TokenInfo, LogoutOptions } from './auth/types';
import { UiPathSDKConfig, PartialUiPathConfig, BaseConfig, hasOAuthConfig, hasSecretConfig } from './config/sdk-config';
import { validateConfig, normalizeBaseUrl, isCompleteConfig } from './config/config-utils';
import { telemetryClient, trackEvent } from './telemetry';
Expand Down Expand Up @@ -288,13 +288,44 @@ export class UiPath implements IUiPath {
/**
* Logout from the SDK, clearing all authentication state.
* After calling this method, the user will need to re-initialize to authenticate again.
*
* By default only local authentication state is cleared — the Automation Cloud
* session is untouched, so a subsequent sign-in completes silently without
* showing a login screen. Pass `endSession: true` to also terminate the
* Automation Cloud session: the browser is redirected to the Identity
* end-session endpoint (invalidating the refresh token as well), so the next
* sign-in prompts for credentials. Note that an already-issued access token
* remains valid until its natural expiry.
*
* The cloud logout requires the `openid` scope in your SDK configuration —
* the SDK sends the OIDC ID token as `id_token_hint` to prove the
* end-session request. Without an ID token the SDK clears local state
* only, logs a warning, and skips the cloud logout.
*
* @param options - Logout behavior options
*
* @example
* ```typescript
* // 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(/\/$/, '')
* });
* ```
*/
public logout(): void {
public logout(options?: LogoutOptions): void {
// Secret-based auth has no session to end — skip silently
if (this.#config && hasSecretConfig(this.#config)) {
return;
}
this.#authService?.logout();
this.#authService?.logout(options);
this.#initialized = false;
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/constants/endpoints/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export const IDENTITY_ENDPOINTS = {
BASE_PATH: `${IDENTITY_BASE}/connect`,
TOKEN: `${IDENTITY_BASE}/connect/token`,
AUTHORIZE: `${IDENTITY_BASE}/connect/authorize`,
END_SESSION: `${IDENTITY_BASE}/connect/endsession`,
} as const;
Loading
Loading