diff --git a/README.md b/README.md index 6f234942..029b2978 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,31 @@ export class MyComponent { } ``` +#### Handling errors + +Whenever the SDK fails to retrieve an Access Token, either as part of the above interceptor or when manually calling `AuthService.getAccessTokenSilently` and `AuthService.getAccessTokenWithPopup`, it will emit the corresponding error in the `AuthService.error$` observable. + +If you want to interact to these errors, subscribe to the `error$` observable and act accordingly. + +``` +ngOnInit() { + this.authService.error$.subscribe(error => { + // Handle Error here + }); +} +``` + +A common reason you might want to handle the above errors, emitted by the `error$` observable, is to re-login the user when the SDK throws a `login_required` error. + +``` +ngOnInit() { + this.authService.error$.pipe( + filter(e => e.error === 'login_required'), + mergeMap(() => this.authService.loginWithRedirect()) + ).subscribe(); +} +``` + ### Dynamic Configuration Instead of using `AuthModule.forRoot` to specify auth configuration, you can provide a factory function using `APP_INITIALIZER` to load your config from an external source before the auth module is loaded, and provide your configuration using `AuthClientConfig.set`: diff --git a/projects/auth0-angular/src/lib/auth.interceptor.spec.ts b/projects/auth0-angular/src/lib/auth.interceptor.spec.ts index acddc607..a57e5ea2 100644 --- a/projects/auth0-angular/src/lib/auth.interceptor.spec.ts +++ b/projects/auth0-angular/src/lib/auth.interceptor.spec.ts @@ -9,13 +9,14 @@ import { import { Data } from '@angular/router'; import { Auth0ClientService } from './auth.client'; import { AuthConfig, HttpMethod, AuthClientConfig } from './auth.config'; +import { AuthService } from './auth.service'; // 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 auth0Client: any; + let authService: Partial; let req: TestRequest; const testData: Data = { message: 'Hello, world' }; @@ -43,8 +44,12 @@ describe('The Auth HTTP Interceptor', () => { let config: Partial; beforeEach(() => { - auth0Client = jasmine.createSpyObj('Auth0Client', ['getTokenSilently']); - auth0Client.getTokenSilently.and.resolveTo('access-token'); + authService = jasmine.createSpyObj('AuthService', [ + 'getAccessTokenSilently', + ]); + (authService.getAccessTokenSilently as jasmine.Spy).and.resolveTo( + 'access-token' + ); config = { httpInterceptor: { @@ -80,7 +85,7 @@ describe('The Auth HTTP Interceptor', () => { useClass: AuthHttpInterceptor, multi: true, }, - { provide: Auth0ClientService, useValue: auth0Client }, + { provide: AuthService, useValue: authService }, { provide: AuthClientConfig, useValue: { get: () => config }, @@ -157,7 +162,7 @@ describe('The Auth HTTP Interceptor', () => { // Testing { uri: /api/addresses } (exact match) assertAuthorizedApiCallTo('/api/addresses', done); - expect(auth0Client.getTokenSilently).toHaveBeenCalledWith({ + expect(authService.getAccessTokenSilently).toHaveBeenCalledWith({ audience: 'audience', scope: 'scope', }); diff --git a/projects/auth0-angular/src/lib/auth.interceptor.ts b/projects/auth0-angular/src/lib/auth.interceptor.ts index 80218f97..163af2a7 100644 --- a/projects/auth0-angular/src/lib/auth.interceptor.ts +++ b/projects/auth0-angular/src/lib/auth.interceptor.ts @@ -16,15 +16,14 @@ import { AuthConfig, } from './auth.config'; -import { Auth0ClientService } from './auth.client'; -import { Auth0Client } from '@auth0/auth0-spa-js'; import { switchMap, first, concatMap, pluck } from 'rxjs/operators'; +import { AuthService } from './auth.service'; @Injectable() export class AuthHttpInterceptor implements HttpInterceptor { constructor( private configFactory: AuthClientConfig, - @Inject(Auth0ClientService) private auth0Client: Auth0Client + private authService: AuthService ) {} intercept( @@ -45,7 +44,9 @@ export class AuthHttpInterceptor implements HttpInterceptor { // outgoing request of(route).pipe( pluck('tokenOptions'), - concatMap((options) => this.auth0Client.getTokenSilently(options)), + concatMap((options) => + this.authService.getAccessTokenSilently(options) + ), switchMap((token: string) => { // Clone the request and attach the bearer token const clone = req.clone({ diff --git a/projects/auth0-angular/src/lib/auth.service.spec.ts b/projects/auth0-angular/src/lib/auth.service.spec.ts index 1aa9ef8c..948f5f21 100644 --- a/projects/auth0-angular/src/lib/auth.service.spec.ts +++ b/projects/auth0-angular/src/lib/auth.service.spec.ts @@ -497,6 +497,32 @@ describe('AuthService', () => { done(); }); }); + + it('should record errors in the error$ observable', (done) => { + const errorObj = new Error('An error has occured'); + + (auth0Client.getTokenSilently as jasmine.Spy).and.rejectWith(errorObj); + + service.getAccessTokenSilently().subscribe(); + + service.error$.subscribe((err: Error) => { + expect(err).toBe(errorObj); + done(); + }); + }); + + it('should bubble errors', (done) => { + const errorObj = new Error('An error has occured'); + + (auth0Client.getTokenSilently as jasmine.Spy).and.rejectWith(errorObj); + + service.getAccessTokenSilently().subscribe({ + error: (err: Error) => { + expect(err).toBe(errorObj); + done(); + }, + }); + }); }); describe('getAccessTokenWithPopup', () => { @@ -516,5 +542,31 @@ describe('AuthService', () => { done(); }); }); + + it('should record errors in the error$ observable', (done) => { + const errorObj = new Error('An error has occured'); + + (auth0Client.getTokenWithPopup as jasmine.Spy).and.rejectWith(errorObj); + + service.getAccessTokenWithPopup().subscribe(); + + service.error$.subscribe((err: Error) => { + expect(err).toBe(errorObj); + done(); + }); + }); + + it('should bubble errors', (done) => { + const errorObj = new Error('An error has occured'); + + (auth0Client.getTokenWithPopup as jasmine.Spy).and.rejectWith(errorObj); + + service.getAccessTokenWithPopup().subscribe({ + error: (err: Error) => { + expect(err).toBe(errorObj); + done(); + }, + }); + }); }); }); diff --git a/projects/auth0-angular/src/lib/auth.service.ts b/projects/auth0-angular/src/lib/auth.service.ts index 335c56e8..ce782794 100644 --- a/projects/auth0-angular/src/lib/auth.service.ts +++ b/projects/auth0-angular/src/lib/auth.service.ts @@ -21,6 +21,7 @@ import { defer, ReplaySubject, merge, + throwError, } from 'rxjs'; import { @@ -250,7 +251,11 @@ export class AuthService implements OnDestroy { ): Observable { return of(this.auth0Client).pipe( concatMap((client) => client.getTokenSilently(options)), - tap(() => this.refreshState$.next()) + tap(() => this.refreshState$.next()), + catchError((error) => { + this.errorSubject$.next(error); + return throwError(error); + }) ); } @@ -271,7 +276,11 @@ export class AuthService implements OnDestroy { ): Observable { return of(this.auth0Client).pipe( concatMap((client) => client.getTokenWithPopup(options)), - tap(() => this.refreshState$.next()) + tap(() => this.refreshState$.next()), + catchError((error) => { + this.errorSubject$.next(error); + return throwError(error); + }) ); }