Skip to content

Commit 263a460

Browse files
Add providerScopes and oauthTokens support to SSO API (#1345)
## Summary - Add `providerScopes` parameter to SSO authorization URL options - Add `oauthTokens` field to SSO profile and token responses - Comprehensive test coverage for new functionality ## Changes Made - Updated `SSOAuthorizationURLOptions` interface to include optional `providerScopes` parameter - Modified `getAuthorizationUrl` method to serialize provider scopes as space-separated string - Added `oauthTokens` field to `ProfileAndToken` and `ProfileAndTokenResponse` interfaces - Updated profile and token serializer to handle oauth tokens deserialization - Added tests for provider scopes in authorization URLs - Added tests for oauth tokens in profile responses ## Test Plan - [x] All existing tests pass - [x] New tests added for provider scopes functionality - [x] New tests added for oauth tokens functionality - [x] Linter passes - [x] TypeScript compilation successful Related to ENT-3768
1 parent 8c6e886 commit 263a460

File tree

5 files changed

+134
-0
lines changed

5 files changed

+134
-0
lines changed

src/sso/interfaces/authorization-url-options.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface SSOAuthorizationURLOptions {
1010
domainHint?: string;
1111
loginHint?: string;
1212
provider?: string;
13+
providerScopes?: string[];
1314
redirectUri: string;
1415
state?: string;
1516
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { UnknownRecord } from '../../common/interfaces/unknown-record.interface';
2+
import {
3+
OauthTokens,
4+
OauthTokensResponse,
5+
} from '../../user-management/interfaces/oauth-tokens.interface';
26
import { Profile, ProfileResponse } from './profile.interface';
37

48
export interface ProfileAndToken<CustomAttributesType extends UnknownRecord> {
59
accessToken: string;
610
profile: Profile<CustomAttributesType>;
11+
oauthTokens?: OauthTokens;
712
}
813

914
export interface ProfileAndTokenResponse<
1015
CustomAttributesType extends UnknownRecord,
1116
> {
1217
access_token: string;
1318
profile: ProfileResponse<CustomAttributesType>;
19+
oauth_tokens?: OauthTokensResponse;
1420
}

src/sso/serializers/profile-and-token.serializer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { UnknownRecord } from '../../common/interfaces/unknown-record.interface';
2+
import { deserializeOauthTokens } from '../../user-management/serializers/oauth-tokens.serializer';
23
import { ProfileAndToken, ProfileAndTokenResponse } from '../interfaces';
34
import { deserializeProfile } from './profile.serializer';
45

@@ -9,4 +10,5 @@ export const deserializeProfileAndToken = <
910
): ProfileAndToken<CustomAttributesType> => ({
1011
accessToken: profileAndToken.access_token,
1112
profile: deserializeProfile(profileAndToken.profile),
13+
oauthTokens: deserializeOauthTokens(profileAndToken.oauth_tokens),
1214
});

src/sso/sso.spec.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,38 @@ describe('SSO', () => {
193193
);
194194
});
195195
});
196+
197+
describe('with providerScopes', () => {
198+
it('generates an authorize url with the provided provider scopes', () => {
199+
const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');
200+
201+
const url = workos.sso.getAuthorizationUrl({
202+
provider: 'Google',
203+
providerScopes: ['profile', 'email', 'calendar'],
204+
clientId: 'proj_123',
205+
redirectUri: 'example.com/sso/workos/callback',
206+
});
207+
208+
expect(url).toMatchInlineSnapshot(
209+
`"https://api.workos.com/sso/authorize?client_id=proj_123&provider=Google&provider_scopes=profile+email+calendar&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`,
210+
);
211+
});
212+
213+
it('handles empty provider scopes array', () => {
214+
const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');
215+
216+
const url = workos.sso.getAuthorizationUrl({
217+
provider: 'Google',
218+
providerScopes: [],
219+
clientId: 'proj_123',
220+
redirectUri: 'example.com/sso/workos/callback',
221+
});
222+
223+
expect(url).toMatchInlineSnapshot(
224+
`"https://api.workos.com/sso/authorize?client_id=proj_123&provider=Google&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`,
225+
);
226+
});
227+
});
196228
});
197229

198230
describe('getProfileAndToken', () => {
@@ -280,6 +312,97 @@ describe('SSO', () => {
280312
expect(profile).toMatchSnapshot();
281313
});
282314
});
315+
316+
describe('with oauth tokens in the response', () => {
317+
it('returns the oauth tokens from the profile and token response', async () => {
318+
fetchOnce({
319+
access_token: '01DMEK0J53CVMC32CK5SE0KZ8Q',
320+
profile: {
321+
id: 'prof_123',
322+
idp_id: '123',
323+
organization_id: 'org_123',
324+
connection_id: 'conn_123',
325+
connection_type: 'OktaSAML',
326+
327+
first_name: 'foo',
328+
last_name: 'bar',
329+
role: {
330+
slug: 'admin',
331+
},
332+
groups: ['Admins', 'Developers'],
333+
raw_attributes: {
334+
335+
first_name: 'foo',
336+
last_name: 'bar',
337+
groups: ['Admins', 'Developers'],
338+
},
339+
custom_attributes: {},
340+
},
341+
oauth_tokens: {
342+
access_token: 'oauth_access_token',
343+
refresh_token: 'oauth_refresh_token',
344+
expires_at: 1640995200,
345+
scopes: ['profile', 'email'],
346+
},
347+
});
348+
349+
const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');
350+
const { accessToken, profile, oauthTokens } =
351+
await workos.sso.getProfileAndToken({
352+
code: 'authorization_code',
353+
clientId: 'proj_123',
354+
});
355+
356+
expect(fetch.mock.calls.length).toEqual(1);
357+
expect(accessToken).toBe('01DMEK0J53CVMC32CK5SE0KZ8Q');
358+
expect(profile).toBeDefined();
359+
expect(oauthTokens).toEqual({
360+
accessToken: 'oauth_access_token',
361+
refreshToken: 'oauth_refresh_token',
362+
expiresAt: 1640995200,
363+
scopes: ['profile', 'email'],
364+
});
365+
});
366+
});
367+
368+
describe('without oauth tokens in the response', () => {
369+
it('returns undefined for oauth tokens when not present in response', async () => {
370+
fetchOnce({
371+
access_token: '01DMEK0J53CVMC32CK5SE0KZ8Q',
372+
profile: {
373+
id: 'prof_123',
374+
idp_id: '123',
375+
organization_id: 'org_123',
376+
connection_id: 'conn_123',
377+
connection_type: 'OktaSAML',
378+
379+
first_name: 'foo',
380+
last_name: 'bar',
381+
role: {
382+
slug: 'admin',
383+
},
384+
raw_attributes: {
385+
386+
first_name: 'foo',
387+
last_name: 'bar',
388+
},
389+
custom_attributes: {},
390+
},
391+
});
392+
393+
const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');
394+
const { accessToken, profile, oauthTokens } =
395+
await workos.sso.getProfileAndToken({
396+
code: 'authorization_code',
397+
clientId: 'proj_123',
398+
});
399+
400+
expect(fetch.mock.calls.length).toEqual(1);
401+
expect(accessToken).toBe('01DMEK0J53CVMC32CK5SE0KZ8Q');
402+
expect(profile).toBeDefined();
403+
expect(oauthTokens).toBeUndefined();
404+
});
405+
});
283406
});
284407

285408
describe('getProfile', () => {

src/sso/sso.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export class SSO {
7171
loginHint,
7272
organization,
7373
provider,
74+
providerScopes,
7475
redirectUri,
7576
state,
7677
}: SSOAuthorizationURLOptions): string {
@@ -93,6 +94,7 @@ export class SSO {
9394
domain_hint: domainHint,
9495
login_hint: loginHint,
9596
provider,
97+
provider_scopes: providerScopes?.join(' '),
9698
client_id: clientId,
9799
redirect_uri: redirectUri,
98100
response_type: 'code',

0 commit comments

Comments
 (0)