Authentication and Authorization with Angular 2+

In this tutorial, I'll show you how you can secure your Angular 2+ application. Please, take a look at my previous post (Authorization and Authentication with AngularJS + Jersey) for the backend implementation. 

1. Authentication mechanism

We use JWT (JSON Web Token) authentication mechanism in our application. We have decided to store our JWT in a cookie with HttpOnly (prevent XSS attacks) and Secure (sent over HTTPS only).

In order to prevent CSRF attack we'll use the double submit cookie.  This pattern is defined as sending a random value in both a cookie and as a request header. When a user authenticates to our application via the login page, the API responds with 2 cookies:
  • A cookie (XSRF-TOKEN) set with a strong random value with Secure flag only.  We will instruct Angular HttpClient to read this value and set it as an HTTP header (X-XSRF-TOKEN) for each subsequent request. Since only JavaScript that runs on your domain can read the cookie, your server can be assured that the XHR came from JavaScript running on your domain.
  • A cookie set with our JWT which includes the strong random value (xsrfToken) as a claim.  This cookie is not accessible through JavaScript as explained above. On the server side, a filter checks if the X-XSRF-TOKEN and the xsrfToken match.

2. Authentication Service


import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/map';
import {Observable} from 'rxjs/Observable';
import {CookieService} from 'ngx-cookie';
import {LoggerService} from './logger.service';
import {ApiService} from './api.service';
import {User, LogoutInfo} from '../../model/user';
@Injectable()
export class AuthService {
private currentUserSubject = new BehaviorSubject<User>(new User());
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
private userUrl: string = 'user';
constructor(
private cookieService: CookieService,
private apiService: ApiService,
private loggerService: LoggerService
) {}
currentUser(): Observable<User> {
return this.currentUserSubject.asObservable();
}
isAuthenticated(): Observable<boolean> {
return this.isAuthenticatedSubject.asObservable();
}
setAuthentication(user: User): void {
this.currentUserSubject.next(user);
this.isAuthenticatedSubject.next(true);
this.loggerService.log(`Session set for ${user.contactName}.`);
}
purgeAuthentication(): void {
this.currentUserSubject.next(new User());
this.isAuthenticatedSubject.next(false);
this.loggerService.log('Session removed.');
}
populate() {
return new Promise((resolve, reject) => {
if (this.cookieService.get('vedrax-index-ui-csrf')) {
let url = `${this.userUrl}/current`;
this.apiService.get<User>(url)
.subscribe(
res => {
this.setAuthentication(res);
resolve();
},
err => {
this.loggerService.logObject(err);
this.purgeAuthentication();
resolve();
});
} else {
this.purgeAuthentication();
resolve();
}
});
}
login(username: string, password: string): Observable<User> {
let url = `${this.userUrl}/login`;
return this.apiService.post<User>(url, {userName: username, password: password})
.map(data => {
this.setAuthentication(data);
return data;
});
}
logout(): Observable<LogoutInfo> {
let url = `${this.userUrl}/logout`;
return this.apiService.get<LogoutInfo>(url)
.map(data => {
this.purgeAuthentication();
return data;
});
}
}

2.1 Session Data

Our session data will be stored in our authentication service. We use BehaviorSubject from RxJS. The whole point of using RxJS is to asynchronously update and share session data across our application. This is done by subscribing to the Observable in our components as follow :

this.authService.currentUser()
.takeUntil(this.destroy$) // avoid memory leak in conjunction with ngDestroy
.subscribe(user => {
//do something with session data
});

We change the session data using the next method of the BehaviorSubject inside our helper methods setAuthentication and purgeAuthentication.

2.2 Populate Method

Because we cannot access our JWT cookie from JavaScript, we need to load the session data from an API during initialization of the application.

We create a provider, which will return a Promise, which will be resolved when the request completed. Our populate function will be executed at application initialization process. This function will retrieve and set our session data.

We need a factory in order to hook into app init process and load the session data when csrf cookie is available.

export function authProviderFactory(provider: AuthService) {
return () => provider.populate();
}

We instruct Angular to use it in the init process with APP_INITIALIZER inside the providers section of your @NgModule.

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpModule
],
providers: [
ApiService,
AuthService,
{
provide: APP_INITIALIZER,
useFactory: authProviderFactory,
deps: [AuthService],
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }

2.3 Login

When no session data are available (no cookies, or expired cookies), the application prompts the user to enter his credentials. The server returns the user data along with the authentication cookies when the credentials match.

2.4 Logout

When the user logout, the server invalidate the authentication cookies.

3. Guards

In order to protect a route, we use the canActivate type, which is run before you navigate to a route. Our authentication guard will check if the user is valid otherwise it will navigate to the login route. It also grabs the current URL which will be set as a query parameter.

import {Injectable} from '@angular/core';
import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import {AuthService} from '../services/auth.service';
import {LoggerService} from '../services/logger.service';
import {User} from '../../model/user';
@Injectable()
export class AccessGuard implements CanActivate {
constructor(
private router: Router,
private authService: AuthService,
private loggerService: LoggerService
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
// this will be passed from the route config
// on the data property
const expectedRole = route.data.expectedRole;
return this.authService.currentUser()
.map((user: User) => {
if (!user.id) {
this.loggerService.log(`User not authenticated.`);
this.router.navigate(['/login'], {
queryParams: {
return: state.url
}
});
return false;
}
if (expectedRole) {
if (user.role !== expectedRole) {
this.loggerService.log(`User with role ${user.role} cannot access this resource.`);
this.router.navigate(['/home']);
return false;
}
}
return true;
});
}
}

Now, we can attach this guard to our route:

{
path: 'account',
component: AccountComponent,
canActivate: [AccessGuard]
}
The last task is to redirect the user after login, which is handled in the login component.

import {Component, OnInit, OnDestroy} from '@angular/core';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/operator/takeUntil';
import {Router, ActivatedRoute} from '@angular/router';
import {FormGroup, FormBuilder, Validators} from '@angular/forms';
import {NotificationService} from '../../services/notification.service';
import {AuthService} from '../../services/auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html'
})
export class LoginComponent implements OnInit, OnDestroy {
//Manage Subscription Declaratively
destroy$: Subject<boolean> = new Subject<boolean>();
form: FormGroup;
loading: boolean = false;
return: string = '';
constructor(
private fb: FormBuilder,
private router: Router,
private route: ActivatedRoute,
private authService: AuthService,
private notificationService: NotificationService
) {}
ngOnInit(): void {
// Get the query params for redirecting the user after login
this.route.queryParamMap
.takeUntil(this.destroy$)
.subscribe(params => this.return = params.get('return') || '/home');
this.form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(4)]]
});
}
ngOnDestroy(): void {
this.destroy$.next(true);
// Now let's also unsubscribe from the subject itself:
this.destroy$.unsubscribe();
}
onSubmit(): void {
if (this.form.valid) {
this.loading = true;
const formModel = this.form.value;
let email = formModel.email;
let password = formModel.password;
this.authService.login(email, password)
.takeUntil(this.destroy$)
.subscribe(data => {
this.loading = false;
this.router.navigateByUrl(this.return);
this.notificationService.notify(`Hi ${data.contactName}`);
});
}
}
}
So, it's done. We have a pretty good foundation for implementing a reliable authentication process in our Angular application.

Don't hesitate to leave your comments or maybe to improve my solution.

Thanks

Comments

Popular posts from this blog

Spring JPA : Using Specification with Projection

Chip input using Reactive Form