DEV Community

Ankit
Ankit

Posted on • Originally published at ankitsharmablogs.com on

Policy-Based Authorization In Angular Using JWT

Introduction

In this article, we will create a web application using ASP.NET Core and Angular. We will then implement authentication and policy-based authorization in the application with the help of JWT. The web application will have two roles – Admin and User. The application will have role-based access for each role. We will learn how to configure and validate a JWT.

Take a look at the final application.

Policy-Based Authorization In Angular Using JWT_Demo

Prerequisites

  • Install the latest .NET Core 3.0 SDK from here
  • Install the latest version of Visual Studio 2019 from here

Source Code

Get the source code from GitHub

What is JWT?

JWT stands for JSON Web Token. It is an open standard that defines a compact and self-contained way to securely transfer the data between two parties. JWT is digitally signed hence it can be verified and trusted. JWT is recommended to be used in a scenario when we need to implement Authorization or information exchange. To explore JWT in-depth please refer to the official website of JWT.

Creating the ASP.NET Core application

Open Visual Studio 2019 and click on “Create a new Project”. A “Create a new Project” dialog will open. Select “ASP.NET Core Web Application” and click on Next. Refer to the image shown below.

Policy-Based Authorization In Angular Using JWT

Now you will be at “Configure your new project” screen, provide the name for your application as ngWithJWT and click on create. You will be navigated to “Create a new ASP.NET Core web application” screen. Select “.NET Core” and “ASP.NET Core 3.0” from the dropdowns on the top. Then, select “Angular” project template and click on Create. Refer to the image shown below.

Policy-Based Authorization In Angular Using JWT

This will create our project. The folder structure of our application is shown below.

Policy-Based Authorization In Angular Using JWT

The ClientApp folder contains the Angular code for our application. The Controllers folders will contain our API controllers. The angular components are present inside the ClientApp\src\app folder. The default template contains few Angular components. These components won’t affect our application, but for the sake of simplicity, we will delete fetchdata and counter folders from ClientApp/app/components folder. Also, remove the reference for these two components from the app.module.ts file.

Adding the Model to the Application

Right-click on the project and select Add >> New Folder. Name the folder as Models. Now right-click on the Models folder and select Add >> class. Name the class file as User.cs. This file will contain the User model. Put the following code in this class.

using System;

namespace ngWithJwt.Models
{
    public class User
    {
        public string UserName { get; set; }
        public string FirstName { get; set; }
        public string Password { get; set; }
        public string UserType { get; set; }
    }
}

Similarly, add a new class and name it Policies.cs. This class will define the policies for role-based authorization. Put the following code into it.

using Microsoft.AspNetCore.Authorization;
using System;
namespace ngWithJwt.Models
{
    public static class Policies
    {
        public const string Admin = "Admin";
        public const string User = "User";
        public static AuthorizationPolicy AdminPolicy()
        {
            return new AuthorizationPolicyBuilder().RequireAuthenticatedUser().RequireRole(Admin).Build();
        }
        public static AuthorizationPolicy UserPolicy()
        {
            return new AuthorizationPolicyBuilder().RequireAuthenticatedUser().RequireRole(User).Build();
        }
    }
}

We have also created two policies for authorization. The AdminPolicy method will check for the “Admin” role while validating a request. Similarly, the UserPolicy method will check for the “User” role while validating the request. We will register both of these policies in the ConfigureServices method of Startup.cs file later in this article.

Configure appsettings.json

Add the following lines in appsettings,json file.

"Jwt": {
  "SecretKey": "KqcL7s998JrfFHRP",
  "Issuer": "https://localhost:5001/",
  "Audience": "https://localhost:5001/"
}

Here we have defined a JSON config for the secret key to be used for encryption. We have also defined the issuer and audience for our JWT. You can use the localhost URL as the value for these properties.

We will be using HmacSha256 as our encryption algorithm for JWT. This algorithm requires a key size of 128 bits or 16 bytes. Hence make sure that your key must satisfy this criterion, otherwise, you will get a run time error .

Install NuGet package

We will install the AspNetCore.Authentication.JwtBearer NuGet package to configure ASP.NET Core middleware for JWT authentication and authorization. Navigate to NuGet gallery page for this package. Select the version of .NET Core 3 from the “Version History”. Copy the command from the “package manager” tab. Run this command in the NuGet package manager console of our application. For this application, we are using .NET Core 3.0.0. Therefore, we will run the following command in the package manager console of our application.

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 3.0.0

Refer to the image below.

Policy-Based Authorization In Angular

Adding the Login Controller

We will add a Login Controller to our application which will handle the Login request from the user. Right-click on the Controllers folder and select Add >> New Item. An “Add New Item” dialog box will open. Select Web from the left panel, then select “API Controller Class” from templates panel and put the name as LoginController.cs. Click on Add. Refer to the image below.

Policy-Based Authorization In Angular Using JWT

Open LoginController.cs file and put the following code into it.

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using ngWithJwt.Models;

namespace ngWithJwt.Controllers
{
    [Produces("application/json")]
    [Route("api/[controller]")]
    public class LoginController : Controller
    {
        private readonly IConfiguration _config;

        private List<User> appUsers = new List<User>
        {
            new User {  FirstName = "Admin",  UserName = "admin", Password = "1234", UserType = "Admin" },
            new User {  FirstName = "Ankit",  UserName = "ankit", Password = "1234", UserType = "User" }
        };

        public LoginController(IConfiguration config)
        {
            _config = config;
        }

        [HttpPost]
        [AllowAnonymous]
        public IActionResult Login([FromBody]User login)
        {
            IActionResult response = Unauthorized();
            User user = AuthenticateUser(login);
            if (user != null)
            {
                var tokenString = GenerateJWT(user);
                response = Ok(new
                {
                    token = tokenString,
                    userDetails = user,
                });
            }
            return response;
        }

        User AuthenticateUser(User loginCredentials)
        {
            User user = appUsers.SingleOrDefault(x => x.UserName == loginCredentials.UserName && x.Password == loginCredentials.Password);
            return user;
        }

        string GenerateJWT(User userInfo)
        {
            var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:SecretKey"]));
            var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub, userInfo.UserName),
                new Claim("firstName", userInfo.FirstName.ToString()),
                new Claim("role",userInfo.UserType),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            };

            var token = new JwtSecurityToken(
                issuer: _config["Jwt:Issuer"],
                audience: _config["Jwt:Audience"],
                claims: claims,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: credentials
            );
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
}

Here we have created a list of users called appUsers. We will use this list to verify user credentials. We are using a hard-coded list for simplicity. Ideally, we should store the user credentials in the database and make a DB call to verify the details. The Login method will receive the login details of the user and then verify it by calling the AuthenticateUser method. The AuthenticateUser method will check if the user details exist in the user list appUsers. If the user details exist, then the method will return an object of type User else it will return null. If the user details are verified successfully, we will invoke the GenerateJWT method.

The GenerateJWT method will configure the JWT for our application. We are using HmacSha256 as our encryption algorithm. We will also create claims to be sent as payload with our JWT. The claims will contain info about the user such as UserName, FirstName, and UserType. We will use this information on the client app. In the end, we will create the JWT by specifying details such as issuer, audience, claims, expire and signingCredentials. We are setting the expiry time as 30 mins from the time of the creation of token. We will send the JWT token back to the client with an OK response. If the user details are invalid, we will send an Unauthorized response.

Configuring Startup.cs file

We will configure the request pipeline for the application in the startup.cs file. This will handle the incoming requests for our application. We will add the following code snippet in ConfigureServices method. You can also check the full method definition on GitHub.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.RequireHttpsMetadata = false;
        options.SaveToken = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = Configuration["Jwt:Issuer"],
            ValidAudience = Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"])),
            ClockSkew = TimeSpan.Zero
        };
        services.AddCors();
    });

    services.AddAuthorization(config =>
    {
        config.AddPolicy(Policies.Admin, Policies.AdminPolicy());
        config.AddPolicy(Policies.User, Policies.UserPolicy());
    });

...
  Existing code in the method.
..
}

We are adding the JWT authentication scheme using the AddAuthentication method. We have provided the parameters to validate the JWT. The description for each parameter is as shown below:

  • ValidateIssuer = true: It will verify if the issuer data available in JWT is valid.
  • ValidateAudience = true: It will verify if the audience data available in JWT is valid.
  • ValidateLifetime = true: It will verify if the token has expired or not
  • ValidateIssuerSigningKey = true: It will verify if the signing key is valid and trusted by the server.
  • ValidIssuer: A string value that represents a valid issuer that will be used to check against the token’s issuer We will use the same value as we used while generating JWT.
  • ValidAudience: A string value that represents a valid audience that will be used to check against the token’s audience. We will use the same value which we used while generating JWT.
  • IssuerSigningKey: The secure key used to sign the JWT.
  • ClockSkew: It will set the clock skew to apply when validating the expiry time.

The default value for ClockSkew is 300 seconds i.e. 5 mins. This means that a default value of 5 mins will be added to the expiry time of JWT. E.g. in our case we have set the expiry time as 30 mins but actually it will be 30 + 5 = 35 mins. Hence, we have set the ClockSkew to zero so that the expiry time of JWT will remain the same as we set while generating it.

We will register our authorization policies in the ConfigureServices method. The extension method AddAuthorization is used to add authorization policy services. To apply these policies, we will use the Authorize attribute on any controller or endpoint we want to secure.

To enable authentication and authorization in our application, we will add the UseAuthentication and UseAuthorization extension methods in the Configure method of Startup class. The code snippet is shown below. You can also check the full method definition on GitHub.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    // Existing code
    ...

    if (!env.IsDevelopment())
    {
        app.UseSpaStaticFiles();
    }
    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller}/{action=Index}/{id?}");
    });

    ...
    // Existing code
    ...
}

Adding the User Controller

We will add a user controller to handle all user-specific requests. Add a new API controller class, UserController.cs and put the following code into it.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ngWithJwt.Models;

namespace ngWithJwt.Controllers
{
    [Produces("application/json")]
    [Route("api/[controller]")]
    public class UserController : Controller
    {
        [HttpGet]
        [Route("GetUserData")]
        [Authorize(Policy = Policies.User)]
        public IActionResult GetUserData()
        {
            return Ok("This is an normal user");
        }

        [HttpGet]
        [Route("GetAdminData")]
        [Authorize(Policy = Policies.Admin)]
        public IActionResult GetAdminData()
        {
            return Ok("This is an Admin user");
        }
    }
}

We have defined two methods, one for each role. Both the method will return a simple string to the client. We are using the Authorize attribute with the policy name to implement policy-based authorization to our controller methods.

Working on the Client side of the application

The code for the client-side is available in the ClientApp folder. We will use Angular CLI to work with the client code.

Using Angular CLI is not mandatory. I am using Angular CLI here as it is user-friendly and easy to use. If you don’t want to use CLI then you can create the files for components and services manually.

Install Angular CLI

You can skip this step if Angular CLI is already installed in your machine. Open a command window and execute the following command to install angular CLI in your machine.

npm install -g @angular/CLI

After the Angular CLI is installed successfully, navigate to ngWithJWT\ClientApp folder and open a command window. We will execute all our Angular CLI commands in this window.

Create the models

Create a folder called models inside the ClientApp\src\app folder. Now we will create a file user.ts in the models folder. Put the following code in it.

export class User {
    userName: string;
    firstName: string;
    isLoggedIn: boolean;
    role: string;
}

Similarly, create another file inside the models folder called roles.ts. This file will contain an enum, which will define roles for our application. Put the following code in it.

export enum UserRole {
    Admin = 'Admin',
    User = 'User'
}

Create the User Service

We will create an Angular service which will convert the Web API response to JSON and pass it to our component. Run the following command.

ng g s services\user

This command will create a folder with name services and then create the following two files inside it.

  1. user.service.ts – the service class file.
  2. user.service.spec.ts – the unit test file for service.

Open user.service.ts file and put the following code inside it.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class UserService {

  myAppUrl = '';

  constructor(private http: HttpClient) {
  }

  getUserData() {
    return this.http.get('/api/user/GetUserData').pipe(map(result => result));
  }

  getAdminData() {
    return this.http.get('/api/user/GetAdminData').pipe(map(result => result));
  }
}

We have defined two methods here to invoke our API endpoints. The method getUserData will invoke the GetUserData method from the User Controller to fetch the user data. The getAdminData method will invoke the GetAdminData method from the User Controller to fetch the Admin data.

Create the Auth Service

Run the following command to create the auth service class.

ng g s services\auth

Open auth.service.ts and put the following code inside it.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { Router } from '@angular/router';
import { User } from '../models/user';

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  userData = new BehaviorSubject<User>(new User());

  constructor(private http: HttpClient, private router: Router) { }

  login(userDetails) {
    return this.http.post<any>('/api/login', userDetails)
      .pipe(map(response => {
        localStorage.setItem('authToken', response.token);
        this.setUserDetails();
        return response;
      }));
  }

  setUserDetails() {
    if (localStorage.getItem('authToken')) {
      const userDetails = new User();
      const decodeUserDetails = JSON.parse(window.atob(localStorage.getItem('authToken').split('.')[1]));

      userDetails.userName = decodeUserDetails.sub;
      userDetails.firstName = decodeUserDetails.firstName;
      userDetails.isLoggedIn = true;
      userDetails.role = decodeUserDetails.role;

      this.userData.next(userDetails);
    }
  }

  logout() {
    localStorage.removeItem('authToken');
    this.router.navigate(['/login']);
    this.userData.next(new User());
  }
}

The login method will invoke the Login API method to validate the user details. If we get an OK response from the API, we will store the JWT in the local storage. The setUserDetails method will extract the payload data from JWT and store it in a BehaviorSubject object called userData. The logout method will clear the local storage and navigate the user back to the login page. It will also reset the userData object.

The Login Component

Run the following command in the command prompt to create the Login component.

ng g c login --module app

The --module flag will ensure that this component will get registered at app.module.ts. Open login.component.ts and put the following code in it.

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { first } from 'rxjs/operators';
import { AuthService } from '../services/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  loading = false;
  loginForm: FormGroup;
  submitted = false;
  returnUrl: string;

  constructor(private formBuilder: FormBuilder,
    private route: ActivatedRoute,
    private router: Router,
    private authService: AuthService) { }

  ngOnInit() {
    this.loginForm = this.formBuilder.group({
      username: ['', Validators.required],
      password: ['', Validators.required]
    });
  }

  get loginFormControl() { return this.loginForm.controls; }

  onSubmit() {
    this.submitted = true;
    if (this.loginForm.invalid) {
      return;
    }
    this.loading = true;
    const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl') || '/';
    this.authService.login(this.loginForm.value)
      .pipe(first())
      .subscribe(
        () => {
          this.router.navigate([returnUrl]);
        },
        () => {
          this.loading = false;
          this.loginForm.reset();
          this.loginForm.setErrors({
            invalidLogin: true
          });
        });
  }
}

Open login.component.html and put the following code in it.

<div class="col-md-6 offset-md-3 mt-5">
  <div *ngIf="loginForm.errors?.invalidLogin" class="alert alert-danger mt-3 mb-0">
    Username or Password is incorrect.
  </div>
  <div class="card">
    <h5 class="card-header">JWT authentication with Angular and .NET Core 3</h5>
    <div class="card-body">
      <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label for="username">Username</label>
          <input type="text" formControlName="username" class="form-control" tabindex="1" autofocus
            [ngClass]="{ 'is-invalid': submitted && loginFormControl.username.errors }" />
          <div *ngIf="submitted && loginFormControl.username.errors" class="invalid-feedback">
            <div *ngIf="loginFormControl.username.errors.required">Username is required</div>
          </div>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" formControlName="password" class="form-control" tabindex="2"
            [ngClass]="{ 'is-invalid': submitted && loginFormControl.password.errors }" />
          <div *ngIf="submitted && loginFormControl.password.errors" class="invalid-feedback">
            <div *ngIf="loginFormControl.password.errors.required">Password is required</div>
          </div>
        </div>
        <button [disabled]="loading" class="btn btn-primary">
          <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
          Login
        </button>
      </form>
    </div>
  </div>
</div>

We are creating a login form having two fields – Username and Password. Both the fields of the form are required fields. The onSubmit method will be called when we click on the Login button. We are using formControlName attribute to bind the input field to the loginForm controls. If the loginForm has invalidLogin error we will display an error message on the screen. We are using Bootstrap for styling the form.

The User-home Component

The UserHomeComponent is accessible to the login with roles defined as “user”. Run the command mentioned below to create this component.

ng g c user-home --module app

Open the user-home.component.ts file and update the UserHomeComponent class by adding the code as shown below.

export class UserHomeComponent implements OnInit {

  userData: string;

  constructor(private userService: UserService) { }

  ngOnInit() {
  }

  fetchUserData() {
    this.userService.getUserData().subscribe(
      (result: string) => {
        this.userData = result;
      }
    );
  }
}

We have defined a method fetchUserData which will invoke the getUserData method of the UserService class. This will return a string and we will store the result in a variable userData of type string.

Open the user-home.component.html file and put the following code inside it.

<h2>This is User home page</h2>
<hr />
<button class="btn btn-primary" (click)=fetchUserData()>Fetch Data</button>
<br />
<br />
<div>
    <h4>
        {{userData}}
    </h4>
</div>

We have defined a button and invoking the fetchUserData on the button click. We are displaying the string value fetched from the API inside a <h4> tag.

The Admin-home Component

The AdminHomeComponent is accessible to the login with roles defined as “admin”. Run the command mentioned below to create this component.

ng g c admin-home --module app

Open the admin-home.component.ts file and update the AdminHomeComponent class by adding the code as shown below.

export class AdminHomeComponent implements OnInit {

  adminData: string;

  constructor(private userService: UserService) { }

  ngOnInit() {
  }

  fetchAdminData() {
    this.userService.getAdminData().subscribe(
      (result: string) => {
        this.adminData = result;
      }
    );
  }
}

We have defined a method fetchAdminData which will invoke the getAdminData method of the UserService class. This will return a string and we will store the result in a variable adminData of type string.

Open the admin-home.component.html file and put the following code inside it.

<h2>This is Admin home page</h2>
<hr />
<button class="btn btn-primary" (click)=fetchAdminData()>Fetch Data</button>
<br />
<br />
<div>
    <h4>
        {{adminData}}
    </h4>
</div>

We have defined a button and invoking the fetchAdminData on the button click. We are displaying the string value fetched from the API inside a <h4> tag.

Adding the links in Nav Menu

Open nav-menu.component.ts and update the NavMenuComponent class as shown below.

export class NavMenuComponent {
  isExpanded = false;
  userDataSubscription: any;
  userData = new User();
  userRole = UserRole;

  constructor(private authService: AuthService) {
    this.userDataSubscription = this.authService.userData.asObservable().subscribe(data => {
      this.userData = data;
    });
  }

  collapse() {
    this.isExpanded = false;
  }

  toggle() {
    this.isExpanded = !this.isExpanded;
  }

  logout() {
    this.authService.logout();
  }
}

We are subscribing to the userData subject of the AuthService class in the constructor and setting the value of a local variable userData. We will use the properties of userData object to toggle the nav-menu links in the template file. The logout method will call the logout method of the AuthService class.

We will add the links for our components in the nav menu. Open nav-menu.component.html and remove the links for Counter and Fetch data components and add the following lines.

<li class="nav-item" [routerLinkActive]='["link-active"]'>
    <a class="nav-link text-dark" *ngIf="!userData.isLoggedIn" [routerLink]='["/login"]'>Login</a>
</li>
<li class="nav-item" [routerLinkActive]='["link-active"]'>
    <a class="nav-link text-dark" *ngIf="userData.isLoggedIn && userData.role==userRole.User" [routerLink]='["/user-home"]' [routerLink]=''>UserHome</a>
</li>
<li class="nav-item" [routerLinkActive]='["link-active"]'>
    <a class="nav-link text-dark" *ngIf="userData.isLoggedIn && userData.role==userRole.Admin" [routerLink]='["/admin-home"]' [routerLink]=''>AdminHome</a>
</li>
<li class="nav-item" [routerLinkActive]='["link-active"]'>
    <a class="nav-link text-dark" *ngIf="userData.isLoggedIn" (click)=logout() [routerLink]=''>Logout</a>
</li>
<li class="nav-item" [routerLinkActive]='["link-active"]' *ngIf="userData.isLoggedIn">
    <a class="nav-link text-dark">Welcome <b>{{userData.firstName}}</b> </a>
</li>

Here we are displaying the links for UserHome and AdminHome based on the role of the user. We are also toggling between the Login and Logout button based on the isLoggedIn property of userData object. Clicking on the Login button will navigate the user to the login page whereas clicking on the logout button will invoke the logout method. We will also display the firstName of the user along with a welcome message once the user is authenticated.

Securing the routes with route guards

To protect URL routes from unauthorized access we will create route guards. We will create two guards for protecting user routs and admin routes. Run the following commands to create an admin guard.

ng g g guards/admin --implements CanActivate

Here we are specifying the interfaces to implement using the implements flag. This command will create a folder name guards and add two files admin.guard.ts and admin.guard.spec.ts. Open admin.guard.ts file and put the following code inside it.

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';
import { User } from '../models/user';
import { UserRole } from '../models/roles';

@Injectable({
  providedIn: 'root'
})
export class AdminGuard implements CanActivate {
  userDataSubscription: any;
  userData = new User();
  constructor(private router: Router, private authService: AuthService) {
    this.userDataSubscription = this.authService.userData.asObservable().subscribe(data => {
      this.userData = data;
    });
  }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

    if (this.userData.role === UserRole.Admin) {
      return true;
    }

    this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
    return false;
  }
}

We will subscribe to userData subject in the constructor of the AdminGuard class. We will override the canActivate method of the interface CanActivate. This method will return true if the user role is Admin otherwise it will set the returnUrl and navigate to the login page.

Similarly, we will create another guard to protect user routes. Run the command shown below.

ng g g guards/auth --implements CanActivate

Open auth.guard.ts file and put the following code inside it.

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';
import { User } from '../models/user';
import { UserRole } from '../models/roles';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  userDataSubscription: any;
  userData = new User();
  constructor(private router: Router, private authService: AuthService) {
    this.userDataSubscription = this.authService.userData.asObservable().subscribe(data => {
      this.userData = data;
    });
  }
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

    if (this.userData.role == UserRole.User) {
      return true;
    }

    this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
    return false;
  }
}

This guard is similar to the AdminGuard. We will verify if the user role is User then return true else set the returnUrl and navigate to the login page.

Add Http Interceptor

We will create an HTTP interceptor service to send the authorization token in the header of each API request. This will make sure that only the authorized user can access a particular API. Run the command shown below to create HttpInterceptorService class.

ng g s services\http-interceptor

Open the http-interceptor.service.ts file and put the following code inside it.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class HttpInterceptorService implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = localStorage.getItem('authToken');
    if (token) {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`,
        }
      });
    }
    return next.handle(request)
  }
}

The HttpInterceptorService class will implement the HttpInterceptor interface and override the intercept method. Inside this method, we will fetch the JWT from the localStorage. If the token exists we will modify the request header of the API call by assigning the token value to the Authorization property of headers.

Add Error Interceptor

We will add an error interceptor to intercept and handle the errors gracefully. This will make sure that the user experience is not hampered in case of any error from the server.

Run the command shown below to create ErrorInterceptorService.

ng g s services\error-interceptor

Open the error-interceptor.service.ts file and put the following code inside it.

import { Injectable } from '@angular/core';
import { AuthService } from './auth.service';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class ErrorInterceptorService implements HttpInterceptor {

  constructor(private authService: AuthService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(catchError(err => {
      if (err.status === 401) {
        this.authService.logout();
      }
      return throwError(err.status);
    }));
  }
}

This is similar to HttpInterceptorService. We will implement the HttpInterceptor interface and inside the intercept method, we will catch the error from HttpRequest. If we get error code as 401 i.e. unauthorized from the server, we will invoke the logout method for that user.

In a real-world scenario, we should implement the refresh token feature upon getting an unauthorized error from the server. This will make sure that the user doesn’t get logged out frequently. Implementing refresh token on JWT is beyond the scope of this article but I will try to cover it in future article.

Update App component

Open app.component.ts file and update the AppComponent class as shown below. This will make sure that the logged-in user data won’t be lost on page reload.

export class AppComponent {
  constructor(private authService: AuthService) {
    if (localStorage.getItem('authToken')) {
      this.authService.setUserDetails();
    }
  }
}

Update App module

At last, we will update the app.module.ts file by adding routes for our components and applying auth guards to the routes. We will also add our interceptors in the providers array. Update the app.module.ts file as shown below. You can refer to the GitHub for the complete code of this file.

imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
ReactiveFormsModule,
RouterModule.forRoot([
  { path: '', component: HomeComponent, pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  { path: 'user-home', component: UserHomeComponent, canActivate: [AuthGuard] },
  { path: 'admin-home', component: AdminHomeComponent, canActivate: [AdminGuard] }
])
],
providers: [{ provide: HTTP_INTERCEPTORS, useClass: HttpInterceptorService, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptorService, multi: true },
],
bootstrap: [AppComponent]

Execution Demo

Launch the application by pressing F5 from Visual Studio. You can perform the authentication and authorization as shown in the GIF below.

Policy-Based Authorization In Angular Using JWT_Demo

Summary

We have created an ASP.NET Core application and configured JWT authentication for it. We learned how JWT is configured and validated on the server. The application also has policy-based authorization. We defined two roles for our application and provided role-based access for each role. The client app is created using Angular. We also implemented interceptors for our client to send authorization token in the headers and to handle any error response coming from the server.

See Also

The post Policy-Based Authorization In Angular Using JWT appeared first on Ankit Sharma's Blog.

Top comments (0)