Adding JWT to IdentityServer4 and Angular

Posted on by Roger Versluis.

Introduction

We will add JWT to our ASP.NET Core with IdentityServer4 and Angular setup.

Json Web Tokens (JWT)

Json Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties. It handles authentication and authorization between our IdentityServer4 and our web browser. The information stored in a JWT is trusted and can be verified because it is digitally signed.

IdentityServer4 comes built in with JWT support, for the Angular part we will use the angular-jwt package from auth0 to handle the JWT decoding.

In this article we will implement both Access tokens and Refresh tokens, the flow we will implement goes like this: JWT Flow

Setting up JWT in ASP.NET Core

Open the server solution in Visual Studio and open the Startup.cs file.

First we will have to add some settings to our appsettings.json file, add the following section:

"AppSettings": {
    "Secret": "myverysecretkeythatisverysecretbutreallywillhavetobereplaced",
    "AccessTokenExpiration": 15,
    "RefreshTokenExpiration": 720
}

Replace the Secret setting with a long string, this is your encryption key you use to sign your tokens. Make sure to not forget this token otherwise currently given out tokens cannot be validated anymore.

The AccessTokenExpiration setting indicates how quickly the access token expires. This time is generally kept very low, if a malicious user can get their hands on your access token they will only be able to use it for a maximum of the configured 15 minutes.

When an access token expires it will use the refresh token to renew itself. The refresh token expiration is defined in RefreshTokenExpiration. When the refresh token expiration is up the user is forced to login again and renew both tokens.

To able to read the settings create a new class called AppSettings.cs and give it the following contents:

public class AppSettings
{
    public String Secret { get; set; }
    public Int32 AccessTokenExpiration { get; set; }
    public Int32 RefreshTokenExpiration { get; set; }
}

To make use of the IdentityServer4 JWT support we have to enable it on the Startup.cs class.

Add the following under services.AddMvc:

// Initialize the settings
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);

var appSettings = appSettingsSection.Get<AppSettings>();

var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.RequireHttpsMetadata = false;
        options.SaveToken = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateLifetime = true,

            // ClockSkew of zero is important here to have a strict expiration of the access key
            ClockSkew = TimeSpan.Zero
        };
        options.Events = new JwtBearerEvents
        {
            // When the access token is expired we add a header to the response saying it is expired
            // On the client side we can catch this event and try to refresh the access token.
            OnAuthenticationFailed = context =>
            {
                if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                {
                    context.Response.Headers.Add("AccessToken-Expired", "true");
                }
                return Task.CompletedTask;
            }
        };
    });

And in the Configure method add the following above app.UseMvc:

app.UseAuthentication();

When we login we need to generate an Access Token and a Refresh Token and return it to the user.

Open AuthenticationController.cs and add some extra services to your constructor:

private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly AppSettings _appSettings;
private readonly DataContext _dataContext;

public AuthenticationController(UserManager<IdentityUser> userManager, 
    SignInManager<IdentityUser> signInManager,
    IOptions<AppSettings> appSettings,
    DataContext dataContext)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _appSettings = appSettings.Value;
    _dataContext = dataContext;
}

Now create a new Login method:

[HttpPost("Login")]
public async Task<IActionResult> Login([FromBody]UsersLoginRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    var signInResult = await _signInManager.PasswordSignInAsync(request.UserName, request.Password, true, true);

    if (!signInResult.Succeeded)
    {
        return BadRequest("Invalid user name or password");
    }

    var user = await _userManager.FindByNameAsync(request.UserName);
    var accessToken = GetToken(user.Id, _appSettings.AccessTokenExpiration);

    var refreshToken = GetToken(user.Id, _appSettings.RefreshTokenExpiration);

    var result = new
    {
        AccessToken = accessToken,
        RefreshToken = refreshToken
    };

    return Ok(result);
}

And add a method that will generate our JWT:

private String GetToken(String id, Int32 expiration)
{
    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
    var sKey = new SymmetricSecurityKey(key);
    var tokenDescriptor = new SecurityTokenDescriptor
    {                
        Subject = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, id),
        }),
        NotBefore = DateTime.UtcNow,
        Expires = DateTime.UtcNow.AddMinutes(expiration),
        SigningCredentials = new SigningCredentials(sKey, SecurityAlgorithms.HmacSha256Signature)
    };
    var token = tokenHandler.CreateToken(tokenDescriptor);
    var accessToken = tokenHandler.WriteToken(token);

    return accessToken;
}

We will generate both the Access and Refresh token the same way and send them at login time to the client. Now we use the user ID as the identifier for the refresh token, but in the future it is a good idea to use a separate session table storing refresh token identifiers, this way we can invalidate them if we have to.

Setting up JWT in Angular

We start off by adding a login screen to our Angular application.

Generate the login component first:

ng g c login

login.component.html

<div class="login-wrapper">
    <form class="login" clrForm #loginForm (ngSubmit)="onSubmit()" clrLayout="vertical">
        <section class="title">
            <h3 class="welcome">Welcome to</h3>
            Your angular app
            <h5 class="hint">Use your username to sign in</h5>
        </section>
        <div class="login-group">
            <clr-input-container>
                <input type="text" name="username" clrInput placeholder="Username" [(ngModel)]="username" />
            </clr-input-container>
            <clr-password-container>
                <input type="password" name="password" clrPassword placeholder="Password" [(ngModel)]="password" />
            </clr-password-container>
            <div class="error active" *ngIf="error">
                {{error}}
            </div>
            <button type="submit" class="btn btn-primary" [clrLoading]="loginState">Login</button>
            <a href="/register" class="signup">Sign up</a>
        </div>
    </form>
</div>

login.component.ts

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { ClrLoadingState } from '@clr/angular';
import { HttpErrorResponse } from '@angular/common/http';
import { AuthenticationService } from '../authentication.service';

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

  public username: string;
  public password: string;
  public error: string;
  public loginState = ClrLoadingState.DEFAULT;
  public returnUrl: string;

  constructor(private authentication: AuthenticationService,
    private route: ActivatedRoute,
    private router: Router) { }

  ngOnInit() {
    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
    this.authentication.logout();
  }

  // Login the user, if an error occurs display if on the page
  public onSubmit() {
    this.loginState = ClrLoadingState.LOADING;
    this.error = null;
    this.authentication.login(this.username, this.password).subscribe(() => {
      this.loginState = ClrLoadingState.SUCCESS;
      this.router.navigate([this.returnUrl]);
    }, (error: HttpErrorResponse) => {
      if (error && error.error) {
        this.error = error.error;
      }
      this.loginState = ClrLoadingState.ERROR;
    });
  }
}

Add the logout and login functions to the authentication.service.ts:

public login(username: string, password: string): Observable<{ accessToken: string, refreshToken: string }> {
  return this.http.post<{ accessToken: string, refreshToken: string }>(`/api/authentication/login`, { username, password })
    .pipe(map(result => {
      if (result && result.accessToken && result.refreshToken) {
        localStorage.setItem('access_token', result.accessToken);
        localStorage.setItem('refresh_token', result.refreshToken);
      }
      return result;
    }));
}

public logout(): void {
  localStorage.removeItem('access_token');
  localStorage.removeItem('refresh_token');
}

Make sure to add the /login route to our app-routing.module.ts, your routing array should look like this now:

const routes: Routes = [
  {
    path: 'register',
    component: RegisterComponent
  },
  {
    path: 'login',
    component: LoginComponent
  },
  {
    path: '',
    component: MainNavComponent,
    children: [
      {
        path: '',
        component: HomeComponent
      },
      {
        path: 'random',
        component: RandomNumberComponent
      }
    ]
  }
];

At this point we are able to run the Visual Studio and Angular project. Navigate to /login and try to authenticate. When using a correct password you will see the access_token and refresh_token in your local storage:

Access Token in Local Storage

They look very similar and you can take the value and use a public decoder to see what your JWT contains.

Adding authentication guards

To make sure certain routes aren’t able to accessed when not logged in we will use a guard.

Create a new auth guard:

ng g guard auth

and give it the following content:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(private router: Router) { }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    if (localStorage.getItem('refresh_token') && localStorage.getItem('access_token')) {
      return true;
    }

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

To use the guard we have add the canActivate property to our route:

{
    path: '',
    canActivate: [AuthGuard],
    component: MainNavComponent,
    children: [
      {
        path: '',
        component: HomeComponent
      },
      {
        path: 'random',
        component: RandomNumberComponent
      }
    ]
  }

Every time a route is changed the auth guard will be checked. If no local storage item is found you will be redirected to the login page. If you manually delete the access_token from your local storage and refresh the page you will be redirected to the login page. Because we added canActivate to the route that excluded login and register, those pages will always be accessible whether logged in or not.

Sending the access token

Before continuing in Angular lets make sure authentication works properly on the server side first. This is done by simply adding the Authorize attribute to the Generate method in the RandomGeneratorController:

[Authorize]
[HttpGet]
[Route("Generate")]
public ActionResult<Int32> Generate()
{
    var r = new Random();
    return r.Next(0, 1000);
}

Now when clicking Generate in our app it will return a 401 unauthorized status code. To solve this we have to make sure the access token is sent with every request to the server. Because the access token is only valid for 15 minutes we will want to refresh the access token automatically when it is expires too. To add that functionality we will need to add an interceptor.

Here we will need some extra functionality from the angular-jwt so lets install that first:

npm install -s @auth0/angular-jwt

We’ll need to add a bit of config to make it work, add the following to your imports array in app.module.ts:

JwtModule.forRoot({
  config: {
    tokenGetter: () => localStorage.getItem('access_token'),
    whitelistedDomains: ['localhost:5000', 'localhost:4200'],
    blacklistedRoutes: ['/api/users/authenticate']
  }
})

Create a new file called refresh-token-interceptor.ts and give it the following contents:

import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { catchError, mergeMap } from 'rxjs/operators';
import { JwtInterceptor } from '@auth0/angular-jwt';
import { AuthenticationService } from '../services/authentication.service';

@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService, private jwtInterceptor: JwtInterceptor) {
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (this.jwtInterceptor.isWhitelistedDomain(req) && !this.jwtInterceptor.isBlacklistedRoute(req)) {
            return next.handle(req).pipe(
                catchError(err => {
                    const errorResponse = err as HttpErrorResponse;
                    const expiredHeader = errorResponse.headers.get('AccessToken-Expired') === 'true';

                    if (errorResponse.status === 401 && expiredHeader) {
                        return this.authenticationService.refresh().pipe(mergeMap(r => {
                            if (!r.accessToken) {
                                this.authenticationService.logout();
                                location.reload(true);
                            } else {
                                return this.jwtInterceptor.intercept(req, next);
                            }
                        }));
                    } else {
                        this.authenticationService.logout();
                        location.reload(true);
                    }
                    return throwError(err);
                })
            );
        } else {
            return next.handle(req);
        }
    }
}

Add the refresh function to the authentication.service.ts:

public refresh(): Observable<{ accessToken: string }> {
  const accessToken = localStorage.getItem('access_token');
  const refreshToken = localStorage.getItem('refresh_token');

  return this.http.post<{ accessToken: string }>(`/api/authentication/refresh`, { accessToken, refreshToken })
    .pipe(map((result: { accessToken: string; }) => {
      if (result && result.accessToken) {
        localStorage.setItem('access_token', result.accessToken);
      } else {
        this.logout();
      }
      return result;
    }));
}

And add the refresh method in the AuthenticationController.cs:

[HttpPost("Refresh")]
public async Task<IActionResult> Refresh([FromBody]UsersRefreshRequest request)
{
    var tokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = false,
        ValidateIssuer = false,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appSettings.Secret)),
        ValidateLifetime = true
    };

    var tokenHandler = new JwtSecurityTokenHandler();
    var principal = tokenHandler.ValidateToken(request.RefreshToken, tokenValidationParameters, out var securityToken);

    if (!(securityToken is JwtSecurityToken jwtSecurityToken) ||
        !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
    {
        throw new SecurityTokenException("Invalid accessToken");
    }

    if (principal == null)
    {
        return BadRequest();
    }

    var user = await _userManager.FindByIdAsync(principal.Identity.Name);

    if (user == null)
    {
        return BadRequest();
    }

    var accessToken = GetToken(user.Id, _appSettings.AccessTokenExpiration);

    var result = new
    {
        AccessToken = accessToken
    };

    return Ok(result);
}

To be able to use these interceptors we have to add them to the providers section in app.module.ts:

providers: [
    AuthenticationService,
    JwtInterceptor,
    { provide: HTTP_INTERCEPTORS, useExisting: JwtInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: RefreshTokenInterceptor, multi: true }
],

Now login again and try the generate button a few times. When the access token expires after 15 minutes a 401 is returned from the server. The client responds by refreshing the access token with the refresh token. If this succeeds the original request retried to the server with the new access token and you will get a new random number. Because this process is invisible to the user it is a very seamless experience.

In the next article we will add a session storages and give the user the ability to delete sessions.

You can download the full repository here: https://github.com/farlock85/aspnetcore-angular