abstract class AuthService { // Subject tracks the current token, or is null if no token is currently // available (e.g. refresh pending). private subject = new BehaviorSubject(null); readonly refreshToken: Observable; readonly token: Observable; constructor() { // refreshToken, when subscribed, gets the new token from the backend, // and then completes without values. this.refreshToken = Observable.defer(() => { // Defer allows us to easily execute some action when the Observable // is subscribed. Here, we set the current token to `null` until the // refresh operation is complete. This ensures no requests will be // sent with a known bad token. this.subject.next(null); return this // Next, we refresh the token from the server. .doRefreshToken() // Set it as the active token. .do(token => this.subject.next(token)) // Drop the value, ensuring this Observable only completes when // done and doesn't emit. .ignoreElements() // Finally, share the Observable so we don't attempt multiple // refreshes at once. .shareReplay(); }); // token, when subscribed, returns the latest token. this.token = this // Read the subject (stream of tokens). .subject // Filter out the `null` ones. This part ensure we wait for the next // good token. .filter(token => token !== null) // Take the next good token. .take(1); // There's no current token to start, so refresh to start with. Optionally, // we could set token up to refresh on the first subscription. this.refreshToken.subscribe(); } // Actually refresh the token. Left up to the user. abstract doRefreshToken(): Observable; } class AuthInterceptor implements HttpInterceptor { constructor(private auth: AuthService) { } private addToken(req: HttpRequest): HttpRequest { return req.clone({ headers: req.headers.set('Authorization', `Bearer ${this.auth.getToken()}`) }); } intercept(req: HttpRequest, next: HttpHandler): Observable> { return this .auth // Get the latest token from the auth service. .token // Map the token to a request with the right header set. .map(token => req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) })) // Execute the request on the server. .concatMap(authReq => next.handle(authReq)) // Catch the 401 and handle it by refreshing the token and restarting the chain // (where a new subscription to this.auth.token will get the latest token). .catch((err, restart) => { // If the request is unauthorized, try refreshing the token before restarting. if (err instanceof HttpErrorResponse && err.status === 401) { return Observable.concat(this.auth.refreshToken, restart); } throw err; }); } }