Skip to content

Commit 47dbc61

Browse files
fix: extract access_token from detailed response in HTTP interceptor
When using detailedResponse: true in httpInterceptor tokenOptions, the interceptor was attempting to use the entire response object as the Authorization header value, resulting in 'Bearer [object Object]'. This fix extracts the access_token property from the detailed response object when present, while maintaining backward compatibility with the default string token response. Fixes #731
1 parent 2188c88 commit 47dbc61

File tree

2 files changed

+112
-1
lines changed

2 files changed

+112
-1
lines changed

projects/auth0-angular/src/lib/auth.interceptor.spec.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,107 @@ describe('The Auth HTTP Interceptor', () => {
392392
expect(authState.setError).not.toHaveBeenCalled();
393393
});
394394
}));
395+
396+
it('attach the access token when tokenOptions includes detailedResponse: true', fakeAsync(async (
397+
done: () => void
398+
) => {
399+
// Mock getTokenSilently to return a detailed response object
400+
(
401+
auth0Client.getTokenSilently as unknown as jest.SpyInstance
402+
).mockResolvedValue({
403+
access_token: 'detailed-access-token',
404+
id_token: 'id-token',
405+
expires_in: 86400,
406+
token_type: 'Bearer',
407+
scope: 'openid profile email',
408+
});
409+
410+
// Add a route with detailedResponse: true
411+
config.httpInterceptor!.allowedList!.push({
412+
uri: 'https://my-api.com/api/detailed',
413+
tokenOptions: {
414+
detailedResponse: true,
415+
},
416+
});
417+
418+
httpClient.get('https://my-api.com/api/detailed').subscribe(done);
419+
flush();
420+
await new Promise(process.nextTick);
421+
req = httpTestingController.expectOne('https://my-api.com/api/detailed');
422+
423+
// Should attach only the access_token string, not the whole object
424+
expect(req.request.headers.get('Authorization')).toBe(
425+
'Bearer detailed-access-token'
426+
);
427+
}));
428+
429+
it('stores only the access token string when detailedResponse is used', fakeAsync(async (
430+
done: () => void
431+
) => {
432+
const detailedResponse = {
433+
access_token: 'detailed-token-123',
434+
id_token: 'id-token-456',
435+
expires_in: 3600,
436+
token_type: 'Bearer',
437+
scope: 'openid profile',
438+
};
439+
440+
(
441+
auth0Client.getTokenSilently as unknown as jest.SpyInstance
442+
).mockResolvedValue(detailedResponse);
443+
444+
jest.spyOn(authState, 'setAccessToken');
445+
446+
config.httpInterceptor!.allowedList!.push({
447+
uri: 'https://my-api.com/api/detailed-store',
448+
tokenOptions: {
449+
detailedResponse: true,
450+
},
451+
});
452+
453+
httpClient.get('https://my-api.com/api/detailed-store').subscribe(done);
454+
flush();
455+
await new Promise(process.nextTick);
456+
req = httpTestingController.expectOne(
457+
'https://my-api.com/api/detailed-store'
458+
);
459+
460+
// Should store only the token string, not the whole object
461+
expect(authState.setAccessToken).toHaveBeenCalledWith(
462+
'detailed-token-123'
463+
);
464+
}));
465+
466+
it('handles string token response when detailedResponse is false', fakeAsync(async (
467+
done: () => void
468+
) => {
469+
// Mock getTokenSilently to return a plain string token (default behavior)
470+
(
471+
auth0Client.getTokenSilently as unknown as jest.SpyInstance
472+
).mockResolvedValue('simple-access-token');
473+
474+
jest.spyOn(authState, 'setAccessToken');
475+
476+
config.httpInterceptor!.allowedList!.push({
477+
uri: 'https://my-api.com/api/simple',
478+
tokenOptions: {
479+
detailedResponse: false,
480+
},
481+
});
482+
483+
httpClient.get('https://my-api.com/api/simple').subscribe(done);
484+
flush();
485+
await new Promise(process.nextTick);
486+
req = httpTestingController.expectOne('https://my-api.com/api/simple');
487+
488+
// Should handle string token correctly
489+
expect(req.request.headers.get('Authorization')).toBe(
490+
'Bearer simple-access-token'
491+
);
492+
expect(authState.setAccessToken).toHaveBeenCalledWith(
493+
'simple-access-token'
494+
);
495+
}));
395496
});
396497

397498
describe('Requests that are configured using an uri matcher', () => {

projects/auth0-angular/src/lib/auth.interceptor.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ import {
2525
mergeMap,
2626
mapTo,
2727
pluck,
28+
map,
2829
} from 'rxjs/operators';
29-
import { Auth0Client, GetTokenSilentlyOptions } from '@auth0/auth0-spa-js';
30+
import {
31+
Auth0Client,
32+
GetTokenSilentlyOptions,
33+
GetTokenSilentlyVerboseResponse,
34+
} from '@auth0/auth0-spa-js';
3035
import { Auth0ClientService } from './auth.client';
3136
import { AuthState } from './auth.state';
3237
import { AuthService } from './auth.service';
@@ -113,6 +118,11 @@ export class AuthHttpInterceptor implements HttpInterceptor {
113118
): Observable<string> {
114119
return of(this.auth0Client).pipe(
115120
concatMap((client) => client.getTokenSilently(options)),
121+
map((tokenOrResponse: string | GetTokenSilentlyVerboseResponse) => {
122+
// Extract access_token from detailed response when detailedResponse: true
123+
if (typeof tokenOrResponse === 'string') return tokenOrResponse;
124+
return tokenOrResponse.access_token;
125+
}),
116126
tap((token) => this.authState.setAccessToken(token)),
117127
catchError((error) => {
118128
this.authState.refresh();

0 commit comments

Comments
 (0)