Skip to content

Commit 6a7d025

Browse files
committed
feat: get user details from token
1 parent 64634fa commit 6a7d025

5 files changed

Lines changed: 186 additions & 6 deletions

File tree

docs/authentication.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ useEffect(() => {
151151
- `sdk.isInOAuthCallback()` - Check if processing OAuth redirect
152152
- `sdk.completeOAuth()` - Manually complete OAuth (advanced use)
153153
- `sdk.getToken()` - Get the logged-in user's access token
154+
- `sdk.getTokenIdentity()` - Get identity claims (email, firstName, lastName, preferredUsername, name) decoded from the current JWT access token
154155
- `sdk.logout()` - Logout and clear all authentication state (requires re-initialization to authenticate again)
155156
- `sdk.updateToken()` - Inject a refreshed token into the SDK instance (useful for backend services managing token lifecycle)
156157

src/core/auth/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,14 @@ export interface OAuthContext {
3131
tenantName: string;
3232
scope: string;
3333
}
34+
35+
/**
36+
* Identity claims decoded from the current JWT access token.
37+
*/
38+
export interface TokenIdentity {
39+
email?: string;
40+
firstName?: string;
41+
lastName?: string;
42+
preferredUsername?: string;
43+
name?: string;
44+
}

src/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545

4646
export { UiPath } from './uipath';
4747
export type { UiPathSDKConfig } from './config/sdk-config';
48-
export type { TokenInfo } from './auth/types';
48+
export type { TokenInfo, TokenIdentity } from './auth/types';
4949
export * from './errors';
5050

5151
// Pagination (common across all services)

src/core/uipath.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { UiPathConfig } from './config/config';
22
import { ExecutionContext } from './context/execution';
33
import { AuthService } from './auth/service';
4-
import { TokenInfo } from './auth/types';
4+
import { TokenInfo, TokenIdentity } from './auth/types';
55
import { UiPathSDKConfig, PartialUiPathConfig, BaseConfig, hasOAuthConfig, hasSecretConfig } from './config/sdk-config';
66
import { validateConfig, normalizeBaseUrl, isCompleteConfig } from './config/config-utils';
77
import { telemetryClient, trackEvent } from './telemetry';
88
import { SDKInternalsRegistry } from './internals';
99
import { loadFromMetaTags } from './config/runtime';
1010
import type { IUiPath } from './types';
1111
import { isInActionCenter } from '../utils/platform';
12+
import { decodeBase64 } from '../utils/encoding/base64';
13+
import { AuthenticationError, ValidationError } from './errors';
1214

1315
/**
1416
* UiPath - Core SDK class for authentication and configuration management.
@@ -238,6 +240,65 @@ export class UiPath implements IUiPath {
238240
return this.#authService?.getToken();
239241
}
240242

243+
/**
244+
* Retrieves identity claims of the currently authenticated user by decoding
245+
* the JWT access token held in memory. Does not make an API call.
246+
*
247+
* Returns the following camelCase claims when present on the token:
248+
* `email`, `firstName`, `lastName`, `preferredUsername`, `name`.
249+
*
250+
* @returns The {@link TokenIdentity} extracted from the JWT payload.
251+
* @throws {@link AuthenticationError} If the user is not authenticated.
252+
* @throws {@link ValidationError} If the token is malformed or its payload cannot be decoded.
253+
*
254+
* @example
255+
* ```typescript
256+
* const sdk = new UiPath({ ...config });
257+
* await sdk.initialize();
258+
*
259+
* const identity = sdk.getTokenIdentity();
260+
* console.log(identity.email, identity.name);
261+
* ```
262+
*/
263+
public getTokenIdentity(): TokenIdentity {
264+
if (!this.isAuthenticated()) {
265+
throw new AuthenticationError({
266+
message: 'User is not authenticated. Call initialize() before getTokenIdentity().'
267+
});
268+
}
269+
270+
const token = this.getToken();
271+
if (!token) {
272+
throw new AuthenticationError({
273+
message: 'User is not authenticated. Call initialize() before getTokenIdentity().'
274+
});
275+
}
276+
277+
const segments = token.split('.');
278+
if (segments.length !== 3) {
279+
throw new ValidationError({ message: 'Invalid JWT token format.' });
280+
}
281+
282+
let claims: Record<string, unknown>;
283+
try {
284+
// Convert base64url to base64 and pad to a multiple of 4.
285+
let payload = segments[1].replace(/-/g, '+').replace(/_/g, '/');
286+
const paddingLength = (4 - (payload.length % 4)) % 4;
287+
payload = payload + '='.repeat(paddingLength);
288+
claims = JSON.parse(decodeBase64(payload));
289+
} catch {
290+
throw new ValidationError({ message: 'Failed to decode JWT token payload.' });
291+
}
292+
293+
return {
294+
email: claims.email as string | undefined,
295+
firstName: claims.first_name as string | undefined,
296+
lastName: claims.last_name as string | undefined,
297+
preferredUsername: claims.preferred_username as string | undefined,
298+
name: claims.name as string | undefined
299+
};
300+
}
301+
241302
/**
242303
* Logout from the SDK, clearing all authentication state.
243304
* After calling this method, the user will need to re-initialize to authenticate again.

tests/unit/core/uipath.test.ts

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,28 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
33
import { UiPath } from '../../../src/core/uipath';
44
import { UiPathConfig } from '../../../src/core/config/config';
55
import { ExecutionContext } from '../../../src/core/context/execution';
6+
import { AuthenticationError, ValidationError } from '../../../src/core/errors';
67
import { getConfig, getContext, getTokenManager, getPrivateSDK } from '../../utils/setup';
78
import { TEST_CONSTANTS } from '../../utils/constants/common';
89

910
// ===== MOCKING =====
11+
const mockAuthState = {
12+
token: 'mock-access-token' as string | undefined,
13+
hasValidToken: true
14+
};
15+
1016
const mockTokenManager = {
11-
getToken: () => 'mock-access-token',
12-
hasValidToken: () => true
17+
getToken: () => mockAuthState.token,
18+
hasValidToken: () => mockAuthState.hasValidToken
1319
};
1420

1521
const mockLogout = vi.fn();
1622

1723
vi.mock('../../../src/core/auth/service', () => {
1824
const AuthService: any = vi.fn().mockImplementation(() => ({
1925
getTokenManager: () => mockTokenManager,
20-
hasValidToken: () => true,
21-
getToken: () => 'mock-access-token',
26+
hasValidToken: () => mockAuthState.hasValidToken,
27+
getToken: () => mockAuthState.token,
2228
authenticateWithSecret: vi.fn(),
2329
authenticate: vi.fn().mockResolvedValue(true),
2430
logout: mockLogout
@@ -34,6 +40,13 @@ vi.mock('../../../src/core/auth/service', () => {
3440

3541
vi.mock('../../../src/core/http/api-client');
3642

43+
// ===== TEST HELPERS =====
44+
const createJwt = (payload: Record<string, unknown>): string => {
45+
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url');
46+
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
47+
return `${header}.${body}.signature`;
48+
};
49+
3750
// ===== TEST SUITE =====
3851
describe('UiPath Core', () => {
3952
describe('Constructor', () => {
@@ -352,6 +365,100 @@ describe('UiPath Core', () => {
352365
});
353366
});
354367

368+
describe('getTokenIdentity', () => {
369+
beforeEach(() => {
370+
mockAuthState.hasValidToken = true;
371+
mockAuthState.token = 'mock-access-token';
372+
});
373+
374+
it('should return all 5 mapped identity fields from JWT claims', () => {
375+
mockAuthState.token = createJwt({
376+
email: 'jane.doe@example.com',
377+
first_name: 'Jane',
378+
last_name: 'Doe',
379+
preferred_username: 'jane.doe',
380+
name: 'Jane Doe'
381+
});
382+
383+
const sdk = new UiPath({
384+
baseUrl: TEST_CONSTANTS.BASE_URL,
385+
orgName: TEST_CONSTANTS.ORGANIZATION_ID,
386+
tenantName: TEST_CONSTANTS.TENANT_ID,
387+
secret: TEST_CONSTANTS.CLIENT_SECRET
388+
});
389+
390+
const identity = sdk.getTokenIdentity();
391+
392+
expect(identity).toEqual({
393+
email: 'jane.doe@example.com',
394+
firstName: 'Jane',
395+
lastName: 'Doe',
396+
preferredUsername: 'jane.doe',
397+
name: 'Jane Doe'
398+
});
399+
});
400+
401+
it('should return undefined for missing claims', () => {
402+
mockAuthState.token = createJwt({ email: 'only.email@example.com' });
403+
404+
const sdk = new UiPath({
405+
baseUrl: TEST_CONSTANTS.BASE_URL,
406+
orgName: TEST_CONSTANTS.ORGANIZATION_ID,
407+
tenantName: TEST_CONSTANTS.TENANT_ID,
408+
secret: TEST_CONSTANTS.CLIENT_SECRET
409+
});
410+
411+
const identity = sdk.getTokenIdentity();
412+
413+
expect(identity.email).toBe('only.email@example.com');
414+
expect(identity.firstName).toBeUndefined();
415+
expect(identity.lastName).toBeUndefined();
416+
expect(identity.preferredUsername).toBeUndefined();
417+
expect(identity.name).toBeUndefined();
418+
});
419+
420+
it('should throw AuthenticationError when user is not authenticated', () => {
421+
mockAuthState.hasValidToken = false;
422+
423+
const sdk = new UiPath({
424+
baseUrl: TEST_CONSTANTS.BASE_URL,
425+
orgName: TEST_CONSTANTS.ORGANIZATION_ID,
426+
tenantName: TEST_CONSTANTS.TENANT_ID,
427+
secret: TEST_CONSTANTS.CLIENT_SECRET
428+
});
429+
430+
expect(() => sdk.getTokenIdentity()).toThrow(AuthenticationError);
431+
});
432+
433+
it('should throw ValidationError when token has fewer than 3 segments', () => {
434+
mockAuthState.token = 'not-a-jwt';
435+
436+
const sdk = new UiPath({
437+
baseUrl: TEST_CONSTANTS.BASE_URL,
438+
orgName: TEST_CONSTANTS.ORGANIZATION_ID,
439+
tenantName: TEST_CONSTANTS.TENANT_ID,
440+
secret: TEST_CONSTANTS.CLIENT_SECRET
441+
});
442+
443+
expect(() => sdk.getTokenIdentity()).toThrow(ValidationError);
444+
});
445+
446+
it('should throw ValidationError when payload is not valid JSON', () => {
447+
const header = Buffer.from(JSON.stringify({ alg: 'none' })).toString('base64url');
448+
const invalidPayload = Buffer.from('not-json-content').toString('base64url');
449+
mockAuthState.token = `${header}.${invalidPayload}.signature`;
450+
451+
const sdk = new UiPath({
452+
baseUrl: TEST_CONSTANTS.BASE_URL,
453+
orgName: TEST_CONSTANTS.ORGANIZATION_ID,
454+
tenantName: TEST_CONSTANTS.TENANT_ID,
455+
secret: TEST_CONSTANTS.CLIENT_SECRET
456+
});
457+
458+
expect(() => sdk.getTokenIdentity()).toThrow(ValidationError);
459+
});
460+
});
461+
355462
describe('Multiple Instance Support', () => {
356463
it('should support creating multiple independent instances', () => {
357464
const sdk1 = new UiPath({

0 commit comments

Comments
 (0)