Skip to content

Commit cea90d1

Browse files
authored
fix(auth): add setMultiLogin for login picker (#408)
* fix(auth): make acr_values optional * fix(auth): address acr_values review feedback * fix(auth): use setMultiLogin for login picker * docs(auth): clarify setMultiLogin comments * docs(auth): mark internal multi-login helper * docs(auth): hide setMultiLogin from api docs
1 parent e0d05e3 commit cea90d1

8 files changed

Lines changed: 72 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,4 +429,4 @@ mkdocs serve
429429

430430
[↑ Back to top](#uipath-typescript-sdk)
431431

432-
</div>
432+
</div>

docs/authentication.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,4 @@ async function testAuthentication() {
209209
testAuthentication();
210210
```
211211

212-
Run it: `npx ts-node test-auth.ts`
212+
Run it: `npx ts-node test-auth.ts`

src/core/auth/service.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const GUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12
1212
export class AuthService {
1313
private config: Config;
1414
private tokenManager: TokenManager;
15+
private skipAcrValues: boolean = false;
1516

1617
constructor(config: Config, executionContext: ExecutionContext) {
1718
// Only use stored OAuth context when completing an active callback (URL has ?code=).
@@ -117,6 +118,15 @@ export class AuthService {
117118
return this.tokenManager;
118119
}
119120

121+
/**
122+
* Enables the UiPath login picker during OAuth sign-in.
123+
*
124+
* @internal
125+
*/
126+
public setMultiLogin(): void {
127+
this.skipAcrValues = true;
128+
}
129+
120130
/**
121131
* Authenticates the user based on the provided SDK configuration.
122132
* This method handles OAuth 2.0 authentication flow only.
@@ -344,7 +354,10 @@ export class AuthService {
344354
state: params.state || this.generateCodeVerifier().slice(0, 16)
345355
});
346356

347-
return `${this.config.baseUrl}/${IDENTITY_ENDPOINTS.AUTHORIZE}?${queryParams.toString()}&acr_values=${acrValues}`;
357+
const authorizeUrl = `${this.config.baseUrl}/${IDENTITY_ENDPOINTS.AUTHORIZE}?${queryParams.toString()}`;
358+
return this.skipAcrValues
359+
? authorizeUrl
360+
: `${authorizeUrl}&acr_values=${acrValues}`;
348361
}
349362

350363
/**
@@ -450,4 +463,4 @@ export class AuthService {
450463
url.searchParams.delete('state');
451464
window.history.replaceState({}, '', url.toString());
452465
}
453-
}
466+
}

src/core/config/sdk-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ export function hasOAuthConfig(config: { clientId?: string; redirectUri?: string
3030
// Type guard to check if config has secret
3131
export function hasSecretConfig(config: { secret?: string }): config is { secret: string } {
3232
return Boolean(config.secret);
33-
}
33+
}

src/core/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export interface IUiPath {
2222
*/
2323
initialize(): Promise<void>;
2424

25+
/**
26+
* Enables the UiPath login picker during OAuth sign-in.
27+
*/
28+
setMultiLogin(): void;
29+
2530
/**
2631
* Check if the SDK has been initialized
2732
*/

src/core/uipath.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class UiPath implements IUiPath {
4747
#authService?: AuthService;
4848
#initialized: boolean = false;
4949
#partialConfig?: PartialUiPathConfig;
50+
#multiLogin: boolean = false;
5051

5152
/** Read-only config for user convenience */
5253
public readonly config!: Readonly<BaseConfig>;
@@ -85,6 +86,9 @@ export class UiPath implements IUiPath {
8586

8687
const executionContext = new ExecutionContext();
8788
this.#authService = new AuthService(internalConfig, executionContext);
89+
if (this.#multiLogin) {
90+
this.#authService.setMultiLogin();
91+
}
8892
this.#config = internalConfig;
8993

9094
// Store internals in SDKInternalsRegistry (not visible on instance)
@@ -183,6 +187,16 @@ export class UiPath implements IUiPath {
183187
}
184188
}
185189

190+
/**
191+
* Enables the UiPath login picker during OAuth sign-in.
192+
*
193+
* @internal
194+
*/
195+
public setMultiLogin(): void {
196+
this.#multiLogin = true;
197+
this.#authService?.setMultiLogin();
198+
}
199+
186200
/**
187201
* Check if the SDK has been initialized
188202
*/

tests/unit/core/auth/service.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ describe('AuthService', () => {
7171
const parsedUrl = new URL(url);
7272
expect(parsedUrl.searchParams.get('acr_values')).toBe(`tenantName:${TEST_CONSTANTS.INVALID_GUID_ORG_ID}`);
7373
});
74+
75+
it('should omit acr_values when multi-login is enabled', () => {
76+
const service = createService(TEST_CONSTANTS.ORGANIZATION_ID);
77+
service.setMultiLogin();
78+
const url = service.getAuthorizationUrl({ clientId, redirectUri, codeChallenge, scope });
79+
const parsedUrl = new URL(url);
80+
expect(parsedUrl.searchParams.has('acr_values')).toBe(false);
81+
expect(url).not.toContain('acr_values=');
82+
});
7483
});
7584

7685
describe('exchangeCode', () => {

tests/unit/core/uipath.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const mockTokenManager = {
1313
};
1414

1515
const mockLogout = vi.fn();
16+
const mockSetMultiLogin = vi.fn();
1617

1718
vi.mock('../../../src/core/auth/service', () => {
1819
const AuthService: any = vi.fn().mockImplementation(() => ({
@@ -21,6 +22,7 @@ vi.mock('../../../src/core/auth/service', () => {
2122
getToken: () => 'mock-access-token',
2223
authenticateWithSecret: vi.fn(),
2324
authenticate: vi.fn().mockResolvedValue(true),
25+
setMultiLogin: mockSetMultiLogin,
2426
logout: mockLogout
2527
}));
2628

@@ -63,6 +65,30 @@ describe('UiPath Core', () => {
6365
expect(sdk.isInitialized()).toBe(false); // OAuth requires initialize()
6466
});
6567

68+
it('should enable multi-login on OAuth config', () => {
69+
mockSetMultiLogin.mockClear();
70+
const sdk = new UiPath({
71+
baseUrl: TEST_CONSTANTS.BASE_URL,
72+
orgName: TEST_CONSTANTS.ORGANIZATION_ID,
73+
tenantName: TEST_CONSTANTS.TENANT_ID,
74+
clientId: TEST_CONSTANTS.CLIENT_ID,
75+
redirectUri: 'http://localhost:3000/callback',
76+
scope: 'offline_access'
77+
});
78+
79+
sdk.setMultiLogin();
80+
81+
expect(mockSetMultiLogin).toHaveBeenCalledOnce();
82+
});
83+
84+
it('should allow multi-login before config is loaded', () => {
85+
mockSetMultiLogin.mockClear();
86+
const sdk = new UiPath();
87+
88+
expect(() => sdk.setMultiLogin()).not.toThrow();
89+
expect(mockSetMultiLogin).not.toHaveBeenCalled();
90+
});
91+
6692
it('should validate required config fields', async () => {
6793
const sdk = new UiPath({
6894
baseUrl: '',

0 commit comments

Comments
 (0)