Skip to content

Commit ff7fcd3

Browse files
Fix: Auto-include openid scope in authentication methods to prevent ID token errors (#1369)
1 parent 87d315b commit ff7fcd3

File tree

2 files changed

+240
-5
lines changed

2 files changed

+240
-5
lines changed

src/core/services/AuthenticationOrchestrator.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,30 @@ import { deepCamelCase } from '../utils';
3535
// Represents the raw user profile returned by an API (snake_case)
3636
type RawUser = { [key: string]: any };
3737

38+
/**
39+
* Ensures the 'openid' scope is included in the scope string.
40+
* This is required for receiving an ID token in the response.
41+
* Follows the same pattern as Auth0.Android and Auth0.Swift SDKs.
42+
*
43+
* When no scope is provided, defaults to 'openid profile email' to match
44+
* the behavior of other Auth0 SDKs and provide a complete user profile.
45+
*
46+
* @param scope - The original scope string (optional)
47+
* @returns A scope string that includes 'openid'
48+
*/
49+
function includeRequiredScope(scope?: string): string {
50+
if (!scope) {
51+
return 'openid profile email';
52+
}
53+
54+
const scopes = scope.split(' ');
55+
if (!scopes.includes('openid')) {
56+
return `openid ${scope}`;
57+
}
58+
59+
return scope;
60+
}
61+
3862
/**
3963
* Orchestrates all direct authentication flows by making calls to the Auth0 Authentication API.
4064
* This class is platform-agnostic and relies on an injected HttpClient.
@@ -107,7 +131,7 @@ export class AuthenticationOrchestrator implements IAuthenticationProvider {
107131
subject_token_type: payload.subjectTokenType,
108132
user_profile: payload.userProfile,
109133
audience: payload.audience,
110-
scope: payload.scope,
134+
scope: includeRequiredScope(payload.scope),
111135
};
112136
const { json, response } =
113137
await this.client.post<NativeCredentialsResponse>(
@@ -131,7 +155,7 @@ export class AuthenticationOrchestrator implements IAuthenticationProvider {
131155
password: payload.password,
132156
realm: payload.realm,
133157
audience: payload.audience,
134-
scope: payload.scope,
158+
scope: includeRequiredScope(payload.scope),
135159
};
136160
const { json, response } =
137161
await this.client.post<NativeCredentialsResponse>(
@@ -150,7 +174,7 @@ export class AuthenticationOrchestrator implements IAuthenticationProvider {
150174
grant_type: 'refresh_token',
151175
client_id: this.clientId,
152176
refresh_token: payload.refreshToken,
153-
scope: payload.scope,
177+
scope: includeRequiredScope(payload.scope),
154178
};
155179
const { json, response } =
156180
await this.client.post<NativeCredentialsResponse>(
@@ -212,7 +236,7 @@ export class AuthenticationOrchestrator implements IAuthenticationProvider {
212236
otp: payload.code,
213237
realm: 'email',
214238
audience: payload.audience,
215-
scope: payload.scope,
239+
scope: includeRequiredScope(payload.scope),
216240
};
217241
const { json, response } =
218242
await this.client.post<NativeCredentialsResponse>(
@@ -234,7 +258,7 @@ export class AuthenticationOrchestrator implements IAuthenticationProvider {
234258
otp: payload.code,
235259
realm: 'sms',
236260
audience: payload.audience,
237-
scope: payload.scope,
261+
scope: includeRequiredScope(payload.scope),
238262
};
239263
const { json, response } =
240264
await this.client.post<NativeCredentialsResponse>(

src/core/services/__tests__/AuthenticationOrchestrator.spec.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,47 @@ describe('AuthenticationOrchestrator', () => {
119119
AuthError
120120
);
121121
});
122+
123+
it('should auto-include openid scope when scope is undefined', async () => {
124+
mockHttpClientInstance.post.mockResolvedValueOnce({
125+
json: tokensResponse,
126+
response: new Response(null, { status: 200 }),
127+
});
128+
await orchestrator.passwordRealm({
129+
username: 'info@auth0.com',
130+
password: 'secret pass',
131+
realm: 'Username-Password-Authentication',
132+
});
133+
134+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
135+
'/oauth/token',
136+
expect.objectContaining({
137+
scope: 'openid profile email',
138+
}),
139+
undefined
140+
);
141+
});
142+
143+
it('should auto-include openid scope when not present in custom scope', async () => {
144+
mockHttpClientInstance.post.mockResolvedValueOnce({
145+
json: tokensResponse,
146+
response: new Response(null, { status: 200 }),
147+
});
148+
await orchestrator.passwordRealm({
149+
username: 'info@auth0.com',
150+
password: 'secret pass',
151+
realm: 'Username-Password-Authentication',
152+
scope: 'offline_access',
153+
});
154+
155+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
156+
'/oauth/token',
157+
expect.objectContaining({
158+
scope: 'openid offline_access',
159+
}),
160+
undefined
161+
);
162+
});
122163
});
123164

124165
describe('refresh token', () => {
@@ -165,6 +206,43 @@ describe('AuthenticationOrchestrator', () => {
165206
AuthError
166207
);
167208
});
209+
210+
it('should auto-include openid scope when scope is undefined', async () => {
211+
mockHttpClientInstance.post.mockResolvedValueOnce({
212+
json: tokensResponse,
213+
response: new Response(null, { status: 200 }),
214+
});
215+
await orchestrator.refreshToken({
216+
refreshToken: 'a refresh token of a user',
217+
});
218+
219+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
220+
'/oauth/token',
221+
expect.objectContaining({
222+
scope: 'openid profile email',
223+
}),
224+
undefined
225+
);
226+
});
227+
228+
it('should auto-include openid scope when not present in custom scope', async () => {
229+
mockHttpClientInstance.post.mockResolvedValueOnce({
230+
json: tokensResponse,
231+
response: new Response(null, { status: 200 }),
232+
});
233+
await orchestrator.refreshToken({
234+
refreshToken: 'a refresh token of a user',
235+
scope: 'offline_access',
236+
});
237+
238+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
239+
'/oauth/token',
240+
expect.objectContaining({
241+
scope: 'openid offline_access',
242+
}),
243+
undefined
244+
);
245+
});
168246
});
169247

170248
describe('revoke token', () => {
@@ -290,6 +368,43 @@ describe('AuthenticationOrchestrator', () => {
290368
undefined
291369
);
292370
});
371+
372+
it('should auto-include openid scope when scope is undefined', async () => {
373+
mockHttpClientInstance.post.mockResolvedValueOnce({
374+
json: tokensResponse,
375+
response: new Response(null, { status: 200 }),
376+
});
377+
await orchestrator.loginWithEmail({
378+
email: 'info@auth0.com',
379+
code: '123456',
380+
});
381+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
382+
'/oauth/token',
383+
expect.objectContaining({
384+
scope: 'openid profile email',
385+
}),
386+
undefined
387+
);
388+
});
389+
390+
it('should auto-include openid scope when not present in custom scope', async () => {
391+
mockHttpClientInstance.post.mockResolvedValueOnce({
392+
json: tokensResponse,
393+
response: new Response(null, { status: 200 }),
394+
});
395+
await orchestrator.loginWithEmail({
396+
email: 'info@auth0.com',
397+
code: '123456',
398+
scope: 'profile email',
399+
});
400+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
401+
'/oauth/token',
402+
expect.objectContaining({
403+
scope: 'openid profile email',
404+
}),
405+
undefined
406+
);
407+
});
293408
});
294409

295410
describe('with SMS connection', () => {
@@ -332,6 +447,62 @@ describe('AuthenticationOrchestrator', () => {
332447
undefined
333448
);
334449
});
450+
451+
it('should auto-include openid scope when scope is undefined', async () => {
452+
mockHttpClientInstance.post.mockResolvedValueOnce({
453+
json: tokensResponse,
454+
response: new Response(null, { status: 200 }),
455+
});
456+
await orchestrator.loginWithSMS({
457+
phoneNumber: '+15555555555',
458+
code: '123456',
459+
});
460+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
461+
'/oauth/token',
462+
expect.objectContaining({
463+
scope: 'openid profile email',
464+
}),
465+
undefined
466+
);
467+
});
468+
469+
it('should auto-include openid scope when not present in custom scope', async () => {
470+
mockHttpClientInstance.post.mockResolvedValueOnce({
471+
json: tokensResponse,
472+
response: new Response(null, { status: 200 }),
473+
});
474+
await orchestrator.loginWithSMS({
475+
phoneNumber: '+15555555555',
476+
code: '123456',
477+
scope: 'offline_access',
478+
});
479+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
480+
'/oauth/token',
481+
expect.objectContaining({
482+
scope: 'openid offline_access',
483+
}),
484+
undefined
485+
);
486+
});
487+
488+
it('should not duplicate openid scope when already present', async () => {
489+
mockHttpClientInstance.post.mockResolvedValueOnce({
490+
json: tokensResponse,
491+
response: new Response(null, { status: 200 }),
492+
});
493+
await orchestrator.loginWithSMS({
494+
phoneNumber: '+15555555555',
495+
code: '123456',
496+
scope: 'openid profile email',
497+
});
498+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
499+
'/oauth/token',
500+
expect.objectContaining({
501+
scope: 'openid profile email',
502+
}),
503+
undefined
504+
);
505+
});
335506
});
336507
});
337508

@@ -595,5 +766,45 @@ describe('AuthenticationOrchestrator', () => {
595766
undefined
596767
);
597768
});
769+
770+
it('should auto-include openid scope when scope is undefined', async () => {
771+
mockHttpClientInstance.post.mockResolvedValueOnce({
772+
json: tokensResponse,
773+
response: new Response(null, { status: 200 }),
774+
});
775+
await orchestrator.exchangeNativeSocial({
776+
subjectToken: 'native_token',
777+
subjectTokenType: 'facebook',
778+
});
779+
780+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
781+
'/oauth/token',
782+
expect.objectContaining({
783+
scope: 'openid profile email',
784+
}),
785+
undefined
786+
);
787+
});
788+
789+
it('should auto-include openid scope when not present in custom scope', async () => {
790+
mockHttpClientInstance.post.mockResolvedValueOnce({
791+
json: tokensResponse,
792+
response: new Response(null, { status: 200 }),
793+
});
794+
await orchestrator.exchangeNativeSocial({
795+
subjectToken: 'native_token',
796+
subjectTokenType: 'facebook',
797+
scope: 'profile email',
798+
});
799+
800+
expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
801+
'/oauth/token',
802+
expect.objectContaining({
803+
scope: 'openid profile email',
804+
}),
805+
undefined
806+
);
807+
});
598808
});
599809
});
810+

0 commit comments

Comments
 (0)