Angular Token Based Authentication using Asp.net Core Web API and JSON Web Token
ASP.NET Core Identity is designed to enable us to easily use a number of different storage providers for our ASP.NET applications. We can use the supplied identity providers that are included with the .NET Framework, or we can implement our own providers.
In this tutorial, we will build a Token-Based Authentication using ASP.NET Core Identity , ASP.NET Core Web API and Angular
With Token-Based Authentication, the client application is not dependent on a specific authentication mechanism. The token is generated by the server and the Web API have some APIs to understand, validate the token and perform the authentication. This approach provides Loose Coupling between client and the Web API.
this toturial is not for beginners, to follow it, you must understand Angular2 and Asp.NET REST Services
Securing our web application consists of two scenarios : Authentication and Authorization
1. Authentication identifies the user. So the user must be registered first, using login and password or third party logins like Facebook, Twitter, etc…
2. Authorization talks about permission for authenticated users
– What is the user (authenticated) allowed to do ?
– What ressources can the user access ?
We have build our back end service using ASP.NET WEB API Core, web api provides an internal authorization server using OWIN MIDDLEWARE
The authorization server and the authentication filter are both called into an OWIN middleware component.
This article is the third part of a series of 3 articles
- Token Based Authentication using Asp.net Core Web Api
- Asp.Net Core Web Api Integration testing using EntityFrameworkCore LocalDb and XUnit2
- Angular Token Based Authentication using Asp.net Core Web API and JSON Web Token
BUILDING WEB API RESSOURCE SERVER AND AUTHORIZATION SERVER
In the first part Token Based Authentication using Asp.net Core Web API, I talked about how to configure an ASP.NET Web API Core Token Based Authentication using JWT. So in this tutorial I will talk about an Angular2 client that connect to the Web Api Authorization server using a JWT Token
BUILDING ANGULAR2 WEB CLIENT
Create an ASP.NET Empty WebSite and structure it as follow
Package.json
"dependencies": { "@angular/common": "~2.4.0", "@angular/compiler": "~2.4.0", "@angular/core": "~2.4.0", "@angular/forms": "~2.4.0", "@angular/http": "~2.4.0", "@angular/platform-browser": "~2.4.0", "@angular/platform-browser-dynamic": "~2.4.0", "@angular/router": "~3.4.0", "systemjs": "0.19.40", "angular2-jwt": "^0.2.3", "bootstrap": "^3.3.7", "core-js": "^2.4.1", "rxjs": "^5.0.1", "zone.js": "^0.7.4" },
Package.json defines some dependencies , so use npm to restore them : npm install
Main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
Here, we import AppModule and bootstrap it using platformBrowserDynamic. Because we run our application on the browser. platformBrowserDynamic use a specific way of bootstrapping the application and is defined in @angular/platform-browser-dynamic
There are many ways to setup an angular2 project (Webpack or System.js), but we can use the angular project template in visual studio 2017
app.module.ts file
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpModule } from '@angular/http'; import { ReactiveFormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { HomeComponent } from './home/home.component'; import { PageNotFoundComponent } from './page-not-found.component'; import { UserModule } from './user/user.module'; import { CommonService } from './shared/common.service'; @NgModule({ imports: [ BrowserModule, HttpModule, ReactiveFormsModule, UserModule, AppRoutingModule ], declarations: [ AppComponent, HomeComponent, PageNotFoundComponent ], providers: [ CommonService ], bootstrap: [AppComponent] }) export class AppModule { }
app.module contains sections like :
- Imports : import dependencies
- declarations : components, …
- providers : injected services
index.html
<!DOCTYPE html> <html> <head> <script>document.write('<base href="' + document.location + '" />');</script> <title>AngularJS 2 Token based Authentication using Asp.net Core Web API and JSON Web Token</title> <base href="/"> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <base href="/"> <link href="node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet" /> <link rel="stylesheet" href="styles.css"> <script src="node_modules/core-js/client/shim.min.js"></script> <script src="node_modules/zone.js/dist/zone.js"></script> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('app/main.js').catch(function (err) { console.error(err); }); </script> </head> <body> <jwt-app>Starting Client Application, please wait ...</jwt-app> </body> </html>
Index.html is the entry point of our SPA application, the selector jwt-app will be replaced dynamically by the template of the current route
app.component.ts
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { IProfile } from './user/user.model'; import { UserService } from './user/user.service'; import { UserProfile } from './user/user.profile'; @Component({ selector: 'jwt-app', templateUrl: './app/app.component.html' }) export class AppComponent implements OnInit { pageTitle: string = 'Welcome to AngularJS 2 Token based Authentication using Asp.net Core Web API and JSON Web Token'; loading: boolean = true; Profile: IProfile; constructor(private authService: UserService, private authProfile: UserProfile, private router: Router) { } ngOnInit(): void { this.Profile = this.authProfile.userProfile; } logOut(): void { this.authService.logout(); this.router.navigateByUrl('/home'); } }
common.service.ts
Lets create a common.service.ts file and CommonService class and decorate it as @Injectable()
import { Injectable } from ‘@angular/core’;
import { Injectable } from '@angular/core'; import { Observable } from "rxjs/Observable"; import 'rxjs/add/observable/throw'; import { Response } from '@angular/http'; @Injectable() export class CommonService { private baseUrl = 'http://localhost:58834/api'; constructor() { } getBaseUrl(): string { return this.baseUrl; } handleFullError(error: Response) { return Observable.throw(error); } handleError(error: Response): Observable<any> { let errorMessage = error.json(); console.error(errorMessage); return Observable.throw(errorMessage.error || 'Server error'); } }
Here, we implement common functionalities such as error handling, baseUrl to call API, (http://localhost:58834/api is the API endpoint)
app-routing.module.ts
import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { HomeComponent } from './home/home.component'; import { PageNotFoundComponent } from './page-not-found.component'; import { AuthGuard } from './common/auth.guard'; @NgModule({ imports: [ RouterModule.forRoot([ { path: 'home', component: HomeComponent }, { path: 'products', canActivate: [AuthGuard], data: { preload: true }, loadChildren: 'app/products/product.module#ProductModule' }, { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ], { useHash: true }) ], exports: [RouterModule] }) export class AppRoutingModule { }
app-routing module enable us to define routing strategy :
- http://localhost:3276/home : loads HomeComponent
- Empty URL : loads HomeComponent also
- No existing URL : loads PageNotFoundComponent
app.component.html
<div> <nav class="navbar navbar-default "> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/">AngularJS 2 Token based Authentication using Asp.net Core Web API and JSON Web Token</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav navbar-right"> <li *ngIf="authService.isAuthenticated()"> <a>Welcome {{ Profile.currentUser.userName }}</a> </li> <li *ngIf="!authService.isAuthenticated()"> <a [routerLink]="['/login']">Log In</a> </li> <li *ngIf="!authService.isAuthenticated()"> <a [routerLink]="['/signup']">Register</a> </li> <li *ngIf="authService.isAuthenticated()"> <span> <a (click)="logOut()">Log Out</a></span> </li> </ul> </div> <div> <ul class="nav navbar-nav"> <li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a [routerLink]="['/home']">Home</a> </li> <li> <a [routerLink]="['/products']">Products</a> </li> </ul> </div> </div> </nav> <div class="container"> <div class="row"> <div class="col-md-10"> <router-outlet></router-outlet> </div> <div class="col-md-2"> <router-outlet name="popup"></router-outlet> </div> </div> </div> </div>
home.component.ts
import { Component } from '@angular/core'; @Component({ templateUrl: './app/home/home.component.html' }) export class HomeComponent { public pageTitle: string = 'Welcome'; }
home.component.html
<div class="panel panel-success"> <div class="panel-heading"> {{pageTitle}} </div> </div>
export interface IProduct { id: number; name: string; description: string; price: number; }
product.service.ts
import { Injectable } from '@angular/core'; import { Http, Response, Headers, RequestOptions } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; import 'rxjs/add/operator/map'; import 'rxjs/add/observable/of'; import { IProduct } from './product.model'; import { UserProfile } from '../user/user.profile' import { CommonService } from '../shared/common.service' @Injectable() export class ProductService { constructor(private http: Http, private authProfile: UserProfile, private commonService: CommonService) { } getProducts(): Observable<IProduct[]> { let url = this.commonService.getBaseUrl() + '/product'; let options = null; let profile = this.authProfile.getProfile(); if (profile != null && profile != undefined) { let headers = new Headers({ 'Authorization': 'Bearer ' + profile.token }); options = new RequestOptions({ headers: headers }); } let data: Observable<IProduct[]> = this.http.get(url, options) .map(res => <IProduct[]>res.json()) .do(data => console.log('getProducts: ' + JSON.stringify(data))) .catch(this.commonService.handleError); return data; } }
ProductService.getProducts is the protected ressource, so to get products users must be logged in first and get a token in the response. and finally the token is send (in the header) with the request to access protected ressource:
if (profile != null && profile != undefined) { let headers = new Headers({ 'Authorization': 'Bearer ' + profile.token }); options = new RequestOptions({ headers: headers });
ProductListComponent is protected by a guard. So we must add a guard to any protected route. Here AuthGuard.canActivate must be equal to true but AuthGuard.canActivate return true only if the user is authenticated and the token is valid.
The server validate the token and send good response if the user is authorized ,
RouterModule.forRoot([ { path: 'home', component: HomeComponent }, { path: 'products', canActivate: [AuthGuard], data: { preload: true }, loadChildren: 'app/products/product.module#ProductModule' }, { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ], { useHash: true })
product-list.component.ts
import { Component, OnInit } from '@angular/core'; import { IProduct } from './product.model'; import { ProductService } from './product.service'; @Component({ templateUrl: './app/products/product-list.component.html', styleUrls: ['./app/products/product-list.component.css'] }) export class ProductListComponent implements OnInit { pageTitle: string = 'Product List'; products: IProduct[]; constructor(private productService: ProductService) { } ngOnInit(): void { this.productService.getProducts() .subscribe(products => this.products = products, error => console.log(error)); } }
product-list.component.html
<div class="panel panel-success"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <div class="row"> <div class="col-md-2"></div> <div class="col-md-4"> </div> </div> <div class="row"> <div class="col-md-6"> </div> </div> <div class="table-responsive"> <table class="table" *ngIf="products && products.length"> <thead> <tr> <th>Id</th> <th>Product</th> <th>Price</th> </tr> </thead> <tbody> <tr *ngFor="let product of products "> <td> {{product.id}} </td> <td> {{product.name}} </td> <td>{{ product.price }}</td> </tr> </tbody> </table> </div> </div> </div>
- Register User
signup.component.ts
import { Component } from '@angular/core'; import { NgForm } from '@angular/forms'; import { Router } from '@angular/router'; import { Http, Response, Headers, RequestOptions } from '@angular/http'; import { UserService } from '../user/user.service'; @Component({ templateUrl: './app/signup/signup.component.html' }) export class SignupComponent { errorMessage: string; pageTitle = 'signup'; constructor(private authService: UserService, private router: Router) { } register(signupForm: NgForm) { if (signupForm && signupForm.valid) { let userName = signupForm.form.value.userName; let password = signupForm.form.value.password; let confirmPassword = signupForm.form.value.confirmPassword; var result = this.authService.register(userName, password, confirmPassword) .subscribe( response => { if (this.authService.redirectUrl) { this.router.navigateByUrl(this.authService.redirectUrl); } else { this.router.navigate(['/']); } }, error => { var results = error['_body']; this.errorMessage = error.statusText + ' ' + error.text(); } ); } else { this.errorMessage = 'Please enter a user name and password.'; }; } }
signup.component.html
<div class="panel panel-success"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <form class="form-horizontal" novalidate (ngSubmit)="register(signupForm)" #signupForm="ngForm" autocomplete="off"> <fieldset> <div class="form-group" [ngClass]="{'has-error': (userNameVar.touched || userNameVar.dirty) && !userNameVar.valid }"> <label class="col-md-2 control-label" for="userNameId">User Name</label> <div class="col-md-8"> <input class="form-control" id="userNameId" type="text" placeholder="User Name (required)" required (ngModel)="userName" name="userName" #userNameVar="ngModel" /> <span class="help-block" *ngIf="(userNameVar.touched || userNameVar.dirty) && userNameVar.errors"> <span *ngIf="userNameVar.errors.required"> User name is required. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (passwordVar.touched || passwordVar.dirty) && !passwordVar.valid }"> <label class="col-md-2 control-label" for="passwordId">Password</label> <div class="col-md-8"> <input class="form-control" id="passwordId" type="password" placeholder="Password (required)" required (ngModel)="password" name="password" #passwordVar="ngModel" /> <span class="help-block" *ngIf="(passwordVar.touched || passwordVar.dirty) && passwordVar.errors"> <span *ngIf="passwordVar.errors.required"> Password is required. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (confirmPasswordVar.touched || confirmPasswordVar.dirty) && !confirmPasswordVar.valid }"> <label class="col-md-2 control-label" for="confirmPasswordId">confirmPassword</label> <div class="col-md-8"> <input class="form-control" id="confirmPasswordId" type="password" placeholder="confirmPassword (required)" required (ngModel)="confirmPassword" name="confirmPassword" #confirmPasswordVar="ngModel" /> <span class="help-block" *ngIf="(confirmPasswordVar.touched || confirmPasswordVar.dirty) && confirmPasswordVar.errors"> <span *ngIf="confirmPasswordVar.errors.required"> confirmPassword is required. </span> </span> </div> </div> <div class="form-group"> <div class="col-md-4 col-md-offset-2"> <span> <button class="btn btn-success" type="submit" style="width:80px;margin-right:10px" [disabled]="!signupForm.valid"> Sign Up </button> </span> <span> <a class="btn btn-default" [routerLink]="['/']"> Cancel </a> </span> </div> </div> </fieldset> </form> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> </div>
2. Authenticate User
login.component.ts
import { Component } from '@angular/core'; import { NgForm } from '@angular/forms'; import { Router } from '@angular/router'; import { UserService } from '../user/user.service'; @Component({ templateUrl: './app/login/login.component.html' }) export class LoginComponent { errorMessage: string; pageTitle = 'Log In'; constructor(private authService: UserService, private router: Router) { } login(loginForm: NgForm) { if (loginForm && loginForm.valid) { let userName = loginForm.form.value.userName; let password = loginForm.form.value.password; var result = this.authService.login(userName, password).subscribe( response => { if (this.authService.redirectUrl) { this.router.navigateByUrl(this.authService.redirectUrl); } else { this.router.navigate(['/products']); } }, error => { var results = error['_body']; this.errorMessage = error.statusText + ' ' + error.text(); }); } else { this.errorMessage = 'Please enter a user name and password.'; }; } }
login.component.html
<div class="panel panel-success"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <form class="form-horizontal" novalidate (ngSubmit)="login(loginForm)" #loginForm="ngForm" autocomplete="off"> <fieldset> <div class="form-group" [ngClass]="{'has-error': (userNameVar.touched || userNameVar.dirty) && !userNameVar.valid }"> <label class="col-md-2 control-label" for="userNameId">User Name</label> <div class="col-md-8"> <input class="form-control" id="userNameId" type="text" placeholder="User Name (required)" required (ngModel)="userName" name="userName" #userNameVar="ngModel" /> <span class="help-block" *ngIf="(userNameVar.touched || userNameVar.dirty) && userNameVar.errors"> <span *ngIf="userNameVar.errors.required"> User name is required. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (passwordVar.touched || passwordVar.dirty) && !passwordVar.valid }"> <label class="col-md-2 control-label" for="passwordId">Password</label> <div class="col-md-8"> <input class="form-control" id="passwordId" type="password" placeholder="Password (required)" required (ngModel)="password" name="password" #passwordVar="ngModel" /> <span class="help-block" *ngIf="(passwordVar.touched || passwordVar.dirty) && passwordVar.errors"> <span *ngIf="passwordVar.errors.required"> Password is required. </span> </span> </div> </div> <div class="form-group"> <div class="col-md-4 col-md-offset-2"> <span> <button class="btn btn-success" type="submit" style="width:80px;margin-right:10px" [disabled]="!loginForm.valid"> Log In </button> </span> <span> <a class="btn btn-default" [routerLink]="['/']"> Cancel </a> </span> </div> </div> </fieldset> </form> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> </div>
3. Auth Gard
auth.guard.ts
import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { UserService } from '../user/user.service'; @Injectable() export class AuthGuard implements CanActivate { constructor(private router: Router, private authService: UserService) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { if (this.authService.isAuthorized()) { return true; } this.authService.redirectUrl = state.url; this.router.navigate(['/login']); return false; } }
user.model.ts
export interface IProfile { token: string; expiration: string; claims: IClaim[]; currentUser: IUser; } interface IClaim { type: string; value: string; } interface IUser { id: string; userName: string; email: string; }
user.module.ts
import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { LoginComponent } from '../login/login.component'; import { SignupComponent } from '../signup/signup.component'; import { AuthGuard } from '../common/auth.guard'; import { SharedModule } from '../shared/shared.module'; import { UserService, UserProfile } from './index' @NgModule({ imports: [ SharedModule, RouterModule.forChild([ { path: 'login', component: LoginComponent }, { path: 'signup', component: SignupComponent }, ]) ], declarations: [ LoginComponent, SignupComponent ], providers: [ UserService, AuthGuard, UserProfile ] }) export class UserModule { }
user.profile.ts
import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { Headers } from '@angular/http'; import { IProfile } from './index' @Injectable() export class UserProfile { userProfile: IProfile = { token: "", expiration: "", currentUser: { id: '', userName: '', email: '' }, claims: null }; constructor(private router: Router) { } setProfile(profile: IProfile): void { var nameid = profile.claims.filter(p => p.type == 'nameid')[0].value; var userName = profile.claims.filter(p => p.type == 'sub')[0].value; var email = profile.claims.filter(p => p.type == 'email')[0].value; sessionStorage.setItem('access_token', profile.token); sessionStorage.setItem('expires_in', profile.expiration); sessionStorage.setItem('nameid', nameid); sessionStorage.setItem('userName', userName); sessionStorage.setItem('email', email); } getProfile(authorizationOnly: boolean = false): IProfile { var accessToken = sessionStorage.getItem('access_token'); if (accessToken) { this.userProfile.token = accessToken; this.userProfile.expiration = sessionStorage.getItem('expires_in'); if (this.userProfile.currentUser == null) { this.userProfile.currentUser = { id: '', userName: '', email: '' } } this.userProfile.currentUser.id = sessionStorage.getItem('nameid'); this.userProfile.currentUser.userName = sessionStorage.getItem('userName'); } return this.userProfile; } resetProfile(): IProfile { sessionStorage.removeItem('access_token'); sessionStorage.removeItem('expires_in'); this.userProfile = { token: "", expiration: "", currentUser: null, claims: null }; return this.userProfile; } }
user.service.ts
import { Injectable } from '@angular/core'; import { Http, Headers, RequestOptions, Response } from '@angular/http'; import { Router } from '@angular/router'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/catch'; import { CommonService } from '../shared/common.service'; import { contentHeaders } from '../common/headers'; import { UserProfile } from './user.profile'; import { IProfile } from './user.model'; @Injectable() export class UserService { redirectUrl: string; errorMessage: string; constructor( private http: Http, private router: Router, private authProfile: UserProfile, private commonService: CommonService) { } isAuthenticated() { let profile = this.authProfile.getProfile(); var validToken = profile.token != "" && profile.token != null; var isTokenExpired = this.isTokenExpired(profile); return validToken && !isTokenExpired; } isAuthorized() { let profile = this.authProfile.getProfile(); var validToken = profile.token != "" && profile.token != null; var isTokenExpired = this.isTokenExpired(profile); return validToken && !isTokenExpired; } isTokenExpired(profile: IProfile) { var expiration = new Date(profile.expiration) return expiration < new Date(); } login(userName: string, password: string) { if (!userName || !password) { return; } let options = new RequestOptions( { headers: contentHeaders }); var credentials = { grant_type: 'password', email: userName, password: password }; let url = this.commonService.getBaseUrl() + '/auth/token'; return this.http.post(url, credentials, options) .map((response: Response) => { var userProfile: IProfile = response.json(); this.authProfile.setProfile(userProfile); return response.json(); }).catch(this.commonService.handleFullError); } register(userName: string, password: string, confirmPassword: string) { if (!userName || !password) { return; } let options = new RequestOptions( { headers: contentHeaders }); var credentials = { email: userName, password: password, confirmPassword: confirmPassword }; let url = this.commonService.getBaseUrl() + '/auth/register'; return this.http.post(url, credentials, options) .map((response: Response) => { return response.json(); }).catch(this.commonService.handleFullError); } logout(): void { this.authProfile.resetProfile(); } }
SEE IT IN ACTION
Open Package Manager Console and run update-database command to generate database
This is the generated database on localdb
Configure Startup multiple project : TokenAuthWebApiCore.Server and TokenAuthWebApiCore.Client
Run F5