Single Page Applications or SPA have become the gold standard for modern web applications. Almost every application contains two major areas: secured and unsecured. Today we will learn how to restrict access to the secured area for non-authorized users in modern Angular (version 2 and higher).
No previous Angular experience is required to complete this tutorial. Chrome browser is recommended. You will be able to download complete source code at the end.
Prerequisites
- NodeJS. You can download latest Node from https://nodejs.org/en/download/
- Angular CLI. Once you have installed Node, open the terminal and run
npm install angular-cli -g
If you are getting access errors on Mac run the same script with sudo.
Setup
Let’s start by creating a new project. Open the terminal and navigate to the place where you want to create your project. Run the following command to generate a basic Angular app:
ng new secure-angular-client-side
Your app will be generated in the secure-angular-client-side folder. Navigate to it and run your app:
cd secure-angular-client-side ng serve
ng serve will start a simple development server on localhost:4200. It will also watch for any code changes and refresh your browser automatically on each change. If you open https://localhost:4200 in your browser you should see this:
At this point you can open the project in your favorite IDE and we can move to the next part, which is generating components for our application.
Components
We will generate 3 components for our application:
- Login component. This component will have a simple login form for the demonstration.
- Shell component. This component will be our app master page and will hold the layout, such as header / footer and content area.
- Dashboard component. This component will represent the application dashboard which should be accessible only by logged in users.
Angular CLI provides us with an easy way to generate components. Run the following commands in the terminal from the root folder of your project:
ng g component login --skip-import ng g component shell --skip-import ng g component dashboard --skip-import
Your components are now created under the app folder. Let’s create a components folder under app and move the newly created components there. At the end your folder structure should look like this:
Open src/app/app.module.ts and add login, shell and dashboard components to declarations:
import {LoginComponent} from './components/login/login.component'; import {ShellComponent} from './components/shell/shell.component'; import {DashboardComponent} from './components/dashboard/dashboard.component'; ... declarations: [ AppComponent, LoginComponent, ShellComponent, DashboardComponent ], ...
Now let’s setup routing for our application. We will also add basic navigation between components to verify our routes work.
Routing
Create the file app.routing.module.ts under app folder and paste in the following contents:
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import {LoginComponent} from './components/login/login.component'; import {DashboardComponent} from './components/dashboard/dashboard.component'; import {ShellComponent} from './components/shell/shell.component'; const routes: Routes = [ { path: '', redirectTo: 'login', pathMatch: 'full' }, { path: 'app', component: ShellComponent, children: [ {path: 'dashboard', component: DashboardComponent}, ] }, { path: 'login', component: LoginComponent }, { path: '**', redirectTo: '/login'}, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], providers: [] }) export class AppRoutingModule { }
Go to src/app/app.module.ts and add AppRoutingModule and FormsModule to imports:
... imports: [ BrowserModule, FormsModule, AppRoutingModule ], ...
Open src/app/app.component.html and replace it’s contents with the following HTML:
<ul> <li> <a [routerLink]="['login']">Login</a> </li> <li> <a [routerLink]="['app/dashboard']">Dashboard</a> </li> </ul> <router-outlet></router-outlet>
app.component is a root component and it has a <router-outlet> where our top-level components will be mounted and some temporary navigation to test that routing works properly.
Now open src/app/components/shell/shell.component.html and replace it contents with:
<div>HEADER</div> <router-outlet></router-outlet>
This is our master page and it will have a header and a <router-outlet> where our child components like DashboardComponent under ShellComponent will be mounted. See src/app/app.routing.module.ts for reference:
... { path: 'app', component: ShellComponent, children: [ {path: 'dashboard', component: DashboardComponent}, ] }, ...
Restart ng serve and verify that you can navigate between dashboard and login with the top menu.
Before we will move to the Authorization part, we will add styling for our login page and shell. Copy and paste the following HTML and CSS snippets into the corresponding files.
src/app/components/login/login.component.html
<div class="login"> <div class="login__form"> <div class="login-field"> <label class="login-field__label">Username</label> <input type="text" class="login-field__input"/> </div> <div class="login-field"> <label class="login-field__label">Password</label> <input type="password" class="login-field__input"/> </div> <button class="login__button">Sign In</button> </div> </div>
src/app/components/login/login.component.css
.login__form { width: 400px; padding: 20px; box-sizing: border-box; background: #fff; border: 1px solid #eee; margin: 120px auto 0 auto; font-family: Arial, sans-serif; } .login-field { margin-bottom: 20px; } .login-field__label { display: block; color:#777; font-size: 14px; margin-bottom: 5px; } .login-field__input { display: block; width: 100%; box-sizing: border-box; border: 1px solid #ccc; padding-left: 5px; height: 30px; font-family: Arial,sans-serif; font-size: 14px; } .login__button { display: block; border: none; height: 50px; color:#fff; font-size: 18px; background: #ff5800; width: 100%; }
src/app/app.component.html (remove everything except router-outlet)
<router-outlet></router-outlet>
src/app/components/shell/shell.component.html
<header class="header"> <a class="header__logout">Logout</a> </header> <router-outlet></router-outlet>
src/app/components/shell/shell.component.css
.header { height: 40px; border-bottom: 1px solid #ccc; } .header__logout, .header__logout:visited { float:left; color:#000; text-decoration: underline; cursor: pointer; font-size: 14px; font-family: Arial,sans-serif; margin-top: 7px; }
src/app/components/dashboard/dashboard.component.html
<h1>Welcome to the Dashboard</h1>
src/app/components/dashboard/dashboard.component.css
h1 { font-size: 22px; font-family: Arial, sans-serif; color: #777; }
Now we need to simulate the login/logout process. We already have a basic login form and a header with a logout button. It’s time to add the AuthService that will be responsible for handling the authorization process and providing the current auth state.
Authorization
Create a services folder under app and create auth.service.ts there. This will be our AuthService. Paste the following contents into that file:
import {Injectable} from '@angular/core'; import {Observable} from 'rxjs/Observable'; @Injectable() export class AuthService { constructor() {} authorize(username: string, password: string): Observable<boolean> { return new Observable<boolean>(observer => { localStorage.setItem('token', '123'); observer.next(true); observer.complete(); }); } unauthorize() { localStorage.removeItem('token'); } isAuthorized() { return !!localStorage.getItem('token'); } }
Also add AuthService to the providers array in app.module.ts:
... providers: [ AuthService ], ...
In the real world authorize method would send a request to an API and parse the response. Now let’s wire up authorize() and unauthorize() methods to the Sign In button on the login screen and the Logout link in the header.
Update login.component.ts.
import { Component, OnInit } from '@angular/core'; import {AuthService} from '../../services/auth.service'; import {Router} from '@angular/router'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { username = ''; password = ''; constructor(private authService: AuthService, private router: Router) { } ngOnInit() { } signIn() { this.authService.authorize(this.username, this.password).subscribe( success => success && this.router.navigate(['app/dashboard']) ); } }
Here we declared 2 fields: username and password which we will bind in the template using [(ngModel)]. Also we added a signIn() method that will call authService.authorize() and in case of success it will redirect the user to the dashboard. Now open login.component.html and add the bind fields and methods that we just created. At the end login.component.html should have the following code:
<div class="login"> <div class="login__form"> <div class="login-field"> <label class="login-field__label">Username</label> <input type="text" [(ngModel)]="username" class="login-field__input"/> </div> <div class="login-field"> <label class="login-field__label">Password</label> <input type="password" [(ngModel)]="password" class="login-field__input"/> </div> <button class="login__button" (click)="signIn()">Sign In</button> </div> </div>
Test to see if it works. Go to https://localhost:4200/login. Enter any login and password, you can even leave them blank, press sign in and you should see our dashboard page.
Now let’s update shell.component.ts:
import { Component, OnInit } from '@angular/core'; import {AuthService} from '../../services/auth.service'; import {Router} from '@angular/router'; @Component({ selector: 'app-shell', templateUrl: './shell.component.html', styleUrls: ['./shell.component.css'] }) export class ShellComponent implements OnInit { constructor(private authService: AuthService, private router: Router) { } ngOnInit() { } logout() { this.authService.unauthorize(); this.router.navigate(['login']); } }
Here we added a logout() method that will call unauthoize() on our authentication service and after that redirect the user back to login. Tweak logout link in shell.component.ts:
... <a class="header__logout" (click)="logout()">Logout</a> ...
..and test that you can logout by clicking Logout on dashboard.
Now we have the complete login/logout flow with two major problems:
- The user can open dashboard without being logged in by typing dashboard URL in the browser.
- The user can open login while logged in by typing login URL in the browser.
Let’s look into how to resolve those issues in an effective way in Angular in the final chapter.
Guards
Guards are a very powerful feature implemented in the latest Angular. Guard is a class that implements one of the interfaces and returns true or false from the corresponding function. There are 5 types of route guards at the moment: CanActivate, CanActivateChild, CanDeactivate, CanLoad and Resolve. In our application we will use CanActivate guard. You can read more about other types of guards from the official Angular documentation: https://angular.io/guide/router#milestone-5-route-guards.
Let’s start by creating a guards folder under app. Then go back to the terminal and run the following commands:
ng g guard auth ng g guard nonauth
You will see the newly created guards under the app folder. Move them into the guards folder.
Open auth.guard.ts. You can see that the Angular CLI generates a CanActivate guard by the default:
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @Injectable() export class AuthGuard implements CanActivate { canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { return true; } }
Currently the guard returns true, so if we put it on some route, it will allow access to the route every time. Notice that we can return an Observable or Promise from that function. This is handy when you need to make API call to decide whether the user can access specific route or not, e.g. fetching permissions from the API.
Open the app.routing.module.ts and assign guards to routes in the following way:
... const routes: Routes = [ { path: '', redirectTo: 'login', pathMatch: 'full' }, { path: 'app', component: ShellComponent, children: [ {path: 'dashboard', component: DashboardComponent}, ], canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent, canActivate: [NonauthGuard] }, { path: '**', redirectTo: '/login'}, ]; ...
Note AuthGuard on ShellComponent. You don’t need to assign it to each child as well – just to the parent route. Child routes will be automatically be guarded by the parent and that’s another reason why we want to have the ShellComponent.
Update the providers array in the app.module.ts:
... providers: [ AuthService, AuthGuard, NonauthGuard ], ...
As a final part let’s add some logic into AuthGuard and NonauthGuard. We need to inject AuthService and Router into both guards and check if the user is authorized in the canActivate function. Depending on that we should either return true allowing him to access the route or redirect him to some page that we know he has access to, e.g. login if user is not authorized or dashboard in the opposite case.
src/app/guards/auth.guard.ts
import { Injectable } from '@angular/core'; import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router} from '@angular/router'; import { Observable } from 'rxjs/Observable'; import {AuthService} from '../services/auth.service'; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { if (this.authService.isAuthorized()) { return true; } this.router.navigate(['login']); return false; } }
src/app/guards/nonauth.guard.ts
import { Injectable } from '@angular/core'; import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router} from '@angular/router'; import { Observable } from 'rxjs/Observable'; import {AuthService} from '../services/auth.service'; @Injectable() export class NonauthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { if (!this.authService.isAuthorized()) { return true; } this.router.navigate(['app/dashboard']); return false; } }
Now we are ready for the final test.
Clean localStorage by opening the browser console and typing localStorage.clear(). Refresh the page. You should land on the login page even if you stayed on the dashboard before. Try to access the dashboard by changing the URL: you will not be able to do this. Every time the app will redirect you back to the login page. Now login to the app and try to go to the login page. Again the app will stay on the dashboard.
Conclusion
This is how you can easily secure any Angular app. Note that guards can be used in different variations, not only to check auth, but also with roles, permissions, etc. You can also have multiple guards on a single route. In that case the route will be activated only if all of them return true.
You can also download the complete example of this app from Github: https://github.com/trailheadtechnology/secure-angular-client-side.git