We're almost done; the last thing we need to do is to provide our client with a HttpInterceptor that will capture the Http 401 - Unauthorized errors and try to refresh the access token accordingly. It goes without saying that we need to find a way to have it trigger only once--to avoid endless attempts--and to resend the failed request in case of success.
From Solution Explorer, right-click to the /ClientApp/app/services/ folder and add a new auth.response.interceptor.ts TypeScript file, filling it with the following content:
import { Injectable, Injector } from "@angular/core";
import { Router } from "@angular/router";
import {
HttpClient,
HttpHandler, HttpEvent, HttpInterceptor,
HttpRequest, HttpResponse, HttpErrorResponse
} from "@angular/common/http";
import { AuthService } from "./auth.service";
import { Observable } from "rxjs";
@Injectable()
export class AuthResponseInterceptor implements HttpInterceptor {
currentRequest: HttpRequest<any>;
auth: AuthService;
constructor(
private injector: Injector,
private router: Router
)
{ }
intercept(
request: HttpRequest<any>,
next: HttpHandler): Observable<HttpEvent<any>> {
this.auth = this.injector.get(AuthService);
var token = (this.auth.isLoggedIn()) ?
this.auth.getAuth()!.token : null;
if (token) {
// save current request
this.currentRequest = request;
return next.handle(request)
.do((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
// do nothing
}
})
.catch(error => {
return this.handleError(error)
});
}
else {
return next.handle(request);
}
}
handleError(err: any) {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
// JWT token might be expired:
// try to get a new one using refresh token
console.log("Token expired. Attempting refresh...");
this.auth.refreshToken()
.subscribe(res => {
if (res) {
// refresh token successful
console.log("refresh token successful");
// re-submit the failed request
var http = this.injector.get(HttpClient);
http.request(this.currentRequest).subscribe(
result => {
// do something
}, error => console.error(error)
);
}
else {
// refresh token failed
console.log("refresh token failed");
// erase current token
this.auth.logout();
// redirect to login page
this.router.navigate(["login"]);
}
}, error => console.log(error));
}
}
return Observable.throw(err);
}
}
That's an impressive amount of TypeScript code, but the included comments should help us properly understand what we did:
- In the intercept() method, the first thing we do is check whether there's a token or not; if we don't, there's no need to do anything, otherwise we do two things:
- Store a reference to the current request in an internal property that can be useful later on
- Set up an event handler that will call the handleError() method in case of HTTP errors
- In the handleError() method, we check whether we're dealing with an HttpErrorResponse with a status code of 401 - Unauthorized, which is what we get whenever we attempt to access a controller's action method shielded with the [Authorize] attribute using an invalid (expired) access token. If the conditions match, we attempt to refresh the token using the refreshToken() method of AuthService we implemented a short while ago and subscribe to it, waiting for the outcome:
- In case of success, we resubmit the request that triggered the response error--which we stored in the this.currentRequest local property
- In case of failure, we perform the logout to clear all the expired tokens from the local storage and then redirect the user back to the login screen