Security is an important aspect of a system. Authentication belong to one of them.
Authentication can be implemented in web applications in multiple ways like session-based, cookie-based, token-based, etc. Our main focus would be looking at token-based authentication.
In token-based authentication, basically user login in into the system and server send acces_token and refresh_token with its expiry if the user is validated.
This part is simple, a user enters into a web application. After a user gets logged in, each API call has to be validated to maintain the data security of an application. We have to send access_token
with each request.
On server-side API, it extracts access_token
from the request, verifies (inside Identity Server) if it is valid or not based on its expiry. And return a data if a token is not expired, or return 401 status if access_token
is expired.
We created services dedicated to making ajax calls. For authorization purposes, we’ve to ensure Auth Header is getting passed with each request.
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable()
export class AppService {
constructor(private http: HttpClient) { }
getCountries() {
const headers = new HttpHeaders();
headers.append('Authorization', `Bearer ${token}`);
return this.http.get('/api/country', { headers });
}
}
for every Service (where we need Authorization), we will have to repeat the logic : It mean , ensure that Auth Header is getting passed with each request. This would violate the DRY (Don’t Repeat Yourself). We should find a better way to do this, maybe something that could help us to intercept the http
request before/after it makes a call to Server.
HttpInterceptor
is useful here to solve this problem. Basically it allows us to intercept http
request / response and give access to manipulate it. Hence our generic logic to add header in the request can be kept here. Basic TokenInterceptor code may look like below.
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { AuthService } from './auth/auth.service';
import { Observable } from 'rxjs';
@Injectable()
export class AppHttpInterceptor implements HttpInterceptor {
// AuthService is holding
constructor(public auth: AuthService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = window.localStorage.get('token');
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(request);
}
}
In the above snippet, we have implements AppHttpInterceptorService
class from HttpInterceptor
a method. Then implemented an intercept
method that provides control to modify the request/response. Thus we have kept adding a token to header part in there.
Extending HTTP_INTECEPTORS
provider is needed to take AppHttpInterceptorService
in action. This can be registered inside your app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppHttpInterceptor } from './AppHttpInterceptor';
@NgModule({
bootstrap: [AppComponent],
imports: [...],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AppHttpInterceptor,
multi: true
}
]
})
export class AppModule {}
Ideally AppHttpInterceptor should take care of the below things :
- Add
Authorization
the header on each request - Allow requests which don’t require token logic at all.
- If the token expires, then it automatically refreshes the token.
Generally access_token
have 60 minutes expiry for security reasons. After 60 minutes token access_token
become invalid and API could return a response with 401 status. To handle this case we’ve to make a new token using refresh_token
and use that token for further API calls.
import { AuthService } from './auth.service';
import { Injectable } from '@angular/core';
import {
HttpRequest, HttpHandler, HttpInterceptor, HttpSentEvent,
HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpUserEvent
} from '@angular/common/http';
import { Observable, BehaviorSubject, throwError} from 'rxjs';
import { catchError, switchMap, finalize, filter, take, map } from 'rxjs/operators';
import { Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
const helper = new JwtHelperService();
// You can add URL which don't need AUTH header
const whiteListUrls = ['login', 'refreshToken'];
@Injectable()
export class CustomHttpInterceptorService implements HttpInterceptor {
constructor(
private auth: AuthService,
private router: Router) {}
// Check expiry of token, first decode token
// extract data, and verify expiry timing wrt currentTime
// should be less that currentTime
private isTokenExpired(token): boolean {
const decoded = token && helper.decodeToken(token);
const date = new Date().getTime();
return decoded && new Date(decoded.exp * 1000).getTime() <= date;
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent |
HttpResponse<any> | HttpUserEvent<any> | any> {
const token = localStorage.get('access_token');
const refresh_token = localStorage.get('access_token');
const isExpired = this.isTokenExpired(token);
// Directly allowed whitelisted URL's
if (whiteListUrls.find(w => request.url.includes(w)) || !isExpired) {
return next.handle(request);
}
// If accessToken is expired
if (isExpired) {
// Retreive new refresh_token
const refreshToken = this.auth.refreshToken(refresh_token).pipe(
// Applying catchError only to refreshToken call
catchError(_ => {
// Logout if refresh call fails
return this.logout() as any;
}),
switchMap((user: any) => {
// when new user token retrieved
if (user) {
// Update token locally
this.auth.updateTokens(user);
// Make the ajax call after by passing `accessToken`
return next.handle(this.addTokenToRequest(request, user.accessToken));
}
// Log out if there is no user
return this.logout() as any;
})
);
return refreshToken as any;
} else {
// Make normal ajax call just by passing token
return next.handle(this.addTokenToRequest(request, token));
}
}
private addTokenToRequest(request: HttpRequest<any>, token: string): HttpRequest<any> {
// Token appended in the request header.
return request.clone({ setHeaders: { Authorization: `Bearer ${token}`}});
}
private logout() {
this.auth.logout();
this.router.navigate(['login']);
}
}