Skip to content

Commit

Permalink
[SDK-2635] Avoid emitting error when calling endpoints using allowAno…
Browse files Browse the repository at this point in the history
…nymous (#180)
  • Loading branch information
frederikprijck authored Jul 12, 2021
1 parent dde2483 commit d9ba5bd
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 112 deletions.
65 changes: 51 additions & 14 deletions projects/auth0-angular/src/lib/auth.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ import {
AuthClientConfig,
HttpInterceptorConfig,
} from './auth.config';
import { AuthService } from './auth.service';
import { of, throwError } from 'rxjs';
import { throwError } from 'rxjs';
import { Auth0Client } from '@auth0/auth0-spa-js';
import { Auth0ClientService } from './auth.client';
import { AuthState } from './auth.state';

// NOTE: Read Async testing: https://github.com/angular/angular/issues/25733#issuecomment-636154553

describe('The Auth HTTP Interceptor', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let authService: Partial<AuthService>;
let auth0Client: Auth0Client;
let req: TestRequest;
let authState: AuthState;
const testData: Data = { message: 'Hello, world' };

const assertAuthorizedApiCallTo = (
Expand Down Expand Up @@ -50,12 +53,13 @@ describe('The Auth HTTP Interceptor', () => {

beforeEach(() => {
req = undefined as any;
authService = jasmine.createSpyObj('AuthService', [
'getAccessTokenSilently',
]);
(authService.getAccessTokenSilently as jasmine.Spy).and.returnValue(
of('access-token')
);

auth0Client = new Auth0Client({
domain: '',
client_id: '',
});

spyOn(auth0Client, 'getTokenSilently').and.resolveTo('access-token');

config = {
httpInterceptor: {
Expand Down Expand Up @@ -102,7 +106,10 @@ describe('The Auth HTTP Interceptor', () => {
useClass: AuthHttpInterceptor,
multi: true,
},
{ provide: AuthService, useValue: authService },
{
provide: Auth0ClientService,
useValue: auth0Client,
},
{
provide: AuthClientConfig,
useValue: { get: () => config },
Expand All @@ -112,6 +119,9 @@ describe('The Auth HTTP Interceptor', () => {

httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);
authState = TestBed.inject(AuthState);

spyOn(authState, 'setError').and.callThrough();
});

afterEach(() => {
Expand Down Expand Up @@ -191,7 +201,7 @@ describe('The Auth HTTP Interceptor', () => {
// Testing { uri: /api/addresses } (exact match)
assertAuthorizedApiCallTo('https://my-api.com/api/addresses', done);

expect(authService.getAccessTokenSilently).toHaveBeenCalledWith({
expect(auth0Client.getTokenSilently).toHaveBeenCalledWith({
audience: 'audience',
scope: 'scope',
});
Expand Down Expand Up @@ -224,7 +234,7 @@ describe('The Auth HTTP Interceptor', () => {
it('does not execute HTTP call when not able to retrieve a token', fakeAsync((
done: () => void
) => {
(authService.getAccessTokenSilently as jasmine.Spy).and.returnValue(
(auth0Client.getTokenSilently as jasmine.Spy).and.returnValue(
throwError({ error: 'login_required' })
);

Expand All @@ -239,12 +249,39 @@ describe('The Auth HTTP Interceptor', () => {
it('does execute HTTP call when not able to retrieve a token but allowAnonymous is set to true', fakeAsync((
done: () => void
) => {
(authService.getAccessTokenSilently as jasmine.Spy).and.returnValue(
(auth0Client.getTokenSilently as jasmine.Spy).and.returnValue(
throwError({ error: 'login_required' })
);

assertPassThruApiCallTo('https://my-api.com/api/orders', done);
}));

it('emit error when not able to retrieve a token but allowAnonymous is set to false', fakeAsync((
done: () => void
) => {
(auth0Client.getTokenSilently as jasmine.Spy).and.callFake(() => {
return Promise.reject({ error: 'login_required' });
});

httpClient.request('get', 'https://my-api.com/api/calendar').subscribe({
error: (err) => expect(err).toEqual({ error: 'login_required' }),
});

httpTestingController.expectNone('https://my-api.com/api/calendar');
flush();

expect(authState.setError).toHaveBeenCalled();
}));

it('does not emit error when not able to retrieve a token but allowAnonymous is set to true', fakeAsync(() => {
(auth0Client.getTokenSilently as jasmine.Spy).and.callFake(() => {
return Promise.reject({ error: 'login_required' });
});

assertPassThruApiCallTo('https://my-api.com/api/orders', () => {
expect(authState.setError).not.toHaveBeenCalled();
});
}));
});

describe('Requests that are configured using an uri matcher', () => {
Expand All @@ -261,7 +298,7 @@ describe('The Auth HTTP Interceptor', () => {
// Testing { uriMatcher: (uri) => uri.indexOf('/api/contact') !== -1 }
assertAuthorizedApiCallTo('https://my-api.com/api/contact', done, 'post');

expect(authService.getAccessTokenSilently).toHaveBeenCalledWith({
expect(auth0Client.getTokenSilently).toHaveBeenCalledWith({
audience: 'audience',
scope: 'scope',
});
Expand Down
60 changes: 45 additions & 15 deletions projects/auth0-angular/src/lib/auth.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@angular/common/http';

import { Observable, from, of, iif, throwError } from 'rxjs';
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';

import {
ApiRouteDefinition,
Expand All @@ -15,15 +15,24 @@ import {
HttpInterceptorConfig,
} from './auth.config';

import { switchMap, first, concatMap, pluck, catchError } from 'rxjs/operators';
import { AuthService } from './auth.service';
import { GetTokenSilentlyOptions } from '@auth0/auth0-spa-js';
import {
switchMap,
first,
concatMap,
pluck,
catchError,
tap,
} from 'rxjs/operators';
import { Auth0Client, GetTokenSilentlyOptions } from '@auth0/auth0-spa-js';
import { Auth0ClientService } from './auth.client';
import { AuthState } from './auth.state';

@Injectable()
export class AuthHttpInterceptor implements HttpInterceptor {
constructor(
private configFactory: AuthClientConfig,
private authService: AuthService
@Inject(Auth0ClientService) private auth0Client: Auth0Client,
private authState: AuthState
) {}

intercept(
Expand All @@ -44,16 +53,19 @@ export class AuthHttpInterceptor implements HttpInterceptor {
// outgoing request
of(route).pipe(
pluck('tokenOptions'),
concatMap<GetTokenSilentlyOptions, Observable<string>>((options) =>
this.authService.getAccessTokenSilently(options).pipe(
catchError((err) => {
if (this.allowAnonymous(route, err)) {
return of('');
}

return throwError(err);
})
)
concatMap<GetTokenSilentlyOptions, Observable<string>>(
(options) => {
return this.getAccessTokenSilently(options).pipe(
catchError((err) => {
if (this.allowAnonymous(route, err)) {
return of('');
}

this.authState.setError(err);
return throwError(err);
})
);
}
),
switchMap((token: string) => {
// Clone the request and attach the bearer token
Expand All @@ -77,6 +89,24 @@ export class AuthHttpInterceptor implements HttpInterceptor {
);
}

/**
* Duplicate of AuthService.getAccessTokenSilently, but with a slightly different error handling.
* Only used internally in the interceptor.
* @param options The options for configuring the token fetch.
*/
private getAccessTokenSilently(
options?: GetTokenSilentlyOptions
): Observable<string> {
return of(this.auth0Client).pipe(
concatMap((client) => client.getTokenSilently(options)),
tap((token) => this.authState.setAccessToken(token)),
catchError((error) => {
this.authState.refresh();
return throwError(error);
})
);
}

/**
* Strips the query and fragment from the given uri
* @param uri The uri to remove the query and fragment from
Expand Down
Loading

0 comments on commit d9ba5bd

Please sign in to comment.