Implementing JSON web token authentication

In order to handle JWT-based token authentication, we need to implement the required middleware for doing these tasks:

  • Generating the JWT tokens upon username/password POST requests coming from our client.
  • Validating any JWT token coming with requests by looking at their headers and cookies

Although ASP.NET Core natively supports JWT tokens, the only available middleware is the one validating the request headers (JwtBearerMiddleware). This leaves us with two choices: manually implement what's missing or rely on a third-party library that does just that. We'll try the hand-made route throughout the rest of this chapter, leaving the other alternative to the following chapter.

The first thing to do is define the required steps we need to take care of:

  1. Implement a custom JWT provider middleware to accept POST requests carrying a username and password, and generate JWT tokens accordingly.
  2. Add it to the HTTP request pipeline, together with a properly configured JwtBearerMiddleware to validate incoming requests containing a JWT in their headers block.
  3. Create an Angular 2 Login form to allow our users to perform the login.
  4. Create an Angular 2 Auth service that will handle login/logout and store the JWT token so it can be reused.
  5. Create an AuthHttp wrapper that will add the JWT (if present) to the headers block of each request.

Sounds like a plan...let's do this.

JWT provider

The first thing we need to do is to add the following packages to our project:

    "Microsoft.IdentityModel.Tokens": "5.0.0", 
    "System.IdentityModel.Tokens.Jwt": "5.0.0" 

As always, this can be done in a number of ways: NuGet, GUI, project.json, and others. We already know how to do that. The most recent version as we write is 5.0.0 for both packages, but we can expect it to change in the near future.

Once done, right-click to the OpenGameListWebApp project and create a /Classes/ folder. This is where we will put our custom implementations. We could also call it /AppCode/, /Infrastructure/, or anything else that we like.

Right-click on the new folder, choose the Add | New Item option, and add a new ASP.NET | Middleware Class, naming it JwtProvider.cs just like in the following screenshot:

JWT provider

The new class will contain the default code for the ASP.NET core middleware class implementation pattern. We need to implement a lot of stuff here, so we'll split the content into several regions to make it more readable and understandable.

Private members

Let's add a private members region, wrapping the existing _next variable and adding the following (new lines highlighted):

#region private members 
private readonly RequestDelegate _next; 
 
// JWT-related members

private TimeSpan TokenExpiration;

private SigningCredentials SigningCredentials;

// EF and Identity members, available through DI

private ApplicationDbContext DbContext;

private UserManager<ApplicationUser> UserManager;

private SignInManager<ApplicationUser> SignInManager;

#endregion Private Members

Don't forget to add the required namespaces as well at the beginning of the file:

using Microsoft.IdentityModel.Tokens; 
using Microsoft.AspNetCore.Identity; 
using OpenGameListWebApp.Data.Users; 
using OpenGameListWebApp.Data; 
using System.Text; 

As we can see, we're defining a number of variables here that we'll be using internally. Most of them will be instantiated in the constructor, either programmatically or by using the dependency injection pattern we've already used several times.

Static members

This region includes the minimum amount of info needed to sign in using a JWT token: a SecurityKey and an Issuer. We also define a TokenEndPoint here, which is the URL path that we will use to process the incoming authentication login requests. To put it in other words, it's the route that the JwtProvider will have to intercept (right before the standard MVC routing strategy) to properly handle the login requests:

#region Static Members 
private static readonly string PrivateKey = "private_key_1234567890"; 
public static readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(PrivateKey)); 
public static readonly string Issuer = "OpenGameListWebApp"; 
public static string TokenEndPoint = "/api/connect/token"; 
#endregion Static Members 

Notice that most of these static members have the public access modifier. That's because we'll be using them outside of this class when we'll have to configure the token verification middleware.

Tip

Hardcoding these values in the provider source code is not ideal in production environments. We did it for the sake of simplicity, yet we should remember to adopt better and most secure approaches, such as storing them within an environment variable or a key management tool.

Constructor

Here's what the Constructor region looks like:

#region Constructor
public JwtProvider(
    RequestDelegate next,
    ApplicationDbContext dbContext,
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager)
{
    _next = next;

    // Instantiate JWT-related members
    TokenExpiration = TimeSpan.FromMinutes(10);
    SigningCredentials =  new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);

    // Instantiate through Dependency Injection
    DbContext = dbContext;
    UserManager = userManager;
    SignInManager = signInManager;
}
#endregion Constructor

Here, we define the JWT token expiration time and encrypt the symmetrical security key that will be used to validate JWTs using a standard HmacSha256 encryption algorithm. We're also instantiating the EF/Identity members through DI, like we have done a number of times.

Public methods

Let's move to the Invoke method, which we conveniently wrapped inside the public methods region:

#region public methods 
public Task Invoke(HttpContext httpContext) 
{ 
   // Check if the request path matches our TokenEndPoint 
   if (!httpContext.Request.Path.Equals(TokenEndPoint, StringComparison.Ordinal)) return _next(httpContext); 
 
  // Check if the current request is a valid POST with the appropriate content type (application/x-www-form-urlencoded) 
  if (httpContext.Request.Method.Equals("POST") && httpContext.Request.HasFormContentType) 
  { 
      // OK: generate token and send it via a json-formatted string 
      return CreateToken(httpContext); 
  } 
  else 
 { 
      // Not OK: output a 400 - Bad request HTTP error. 
      httpContext.Response.StatusCode = 400; 
      return httpContext.Response.WriteAsync("Bad request."); 
  } 
} 
#endregion public methods 

Here, we need to check whether the request path matches the chosen login path. If it does, we continue execution, otherwise we entirely skip the request. Right after that, we need to check whether the current request is a valid form-urlencoded  POST. If that's the case, we call the CreateToken internal method; otherwise, we return a 400 error response.

Private methods

The CreateToken method is where most of the magic takes place. We check the given username and password against our internal Identity database and, depending on the result, generate and return either a JWT token or an appropriate error response:

#region Private Methods
private async Task CreateToken(HttpContext httpContext)
{
    try
    {
        // retrieve the relevant FORM data
        string username = httpContext.Request.Form["username"];
        string password = httpContext.Request.Form["password"];

        // check if there's an user with the given username
        var user = await UserManager.FindByNameAsync(username);
        // fallback to support e-mail address instead of username
        if (user == null && username.Contains("@")) user = await UserManager.FindByEmailAsync(username);

        var success = user != null && await UserManager.CheckPasswordAsync(user, password);
         if (success)
         {
             DateTime now = DateTime.UtcNow;

             // add the registered claims for JWT (RFC7519).
             // For more info, see https:
             //tools.ietf.org/html/rfc7519#section-4.1
             var claims = new[] {
             new Claim(JwtRegisteredClaimNames.Iss, Issuer),
             new Claim(JwtRegisteredClaimNames.Sub, user.Id),
             new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
             new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
             // TODO: add additional claims here
         };

         // Create the JWT and write it to a string
         var token = new JwtSecurityToken(
         claims: claims,
         notBefore: now,
         expires: now.Add(TokenExpiration),
         signingCredentials: SigningCredentials);
         var encodedToken = new JwtSecurityTokenHandler().WriteToken(token);

         // build the json response
         var jwt = new {
             access_token = encodedToken,
             expiration = (int)TokenExpiration.TotalSeconds
         };

         // return token
         httpContext.Response.ContentType = "application/json";
         await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(jwt));
         return;
        }
      }
    catch (Exception ex)
    {
        // TODO: handle errors
        throw ex;
    }

    httpContext.Response.StatusCode = 400;
    await httpContext.Response.WriteAsync("Invalid username or password.");
}
#endregion Private Methods

This will also require the following namespace references:

using System.IdentityModel.Tokens.Jwt; 
using System.Security.Claims; 
using Newtonsoft.Json; 

The code is pretty much self-documented using some inline comments indicating what we're doing here and there. We can see how the username and password are retrieved from the HttpContext and checked using the AspNetCore.Identity UserManager class; if the user exists, we issue a JSON-formatted object containing a JWT token and its expiration time, otherwise we return a HTTP 400 error.

Note

It's also worth noting that, as an additional feature, we configured the method to allow clients to authenticate themselves using their e-mail address in place of the username; we did that to demonstrate how versatile this implementation actually is, since we do have full control over the whole authentication process.

Extension methods

The sample code provided for middleware classes includes a handy extension method that we can use to add our newborn provider to the request pipeline. We don't need to change it, so we'll just wrap it in an extension methods region:

#region Extension Methods
// Extension method used to add the middleware to the HTTP request pipeline.
public static class JwtProviderExtensions
{
    public static IApplicationBuilder UseJwtProvider(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<JwtProvider>();
    }
}
#endregion Extension Methods

Full source code

Here's how our JwtProvider class will look after all this hard work:

using System; 
using System.Text; 
using System.Threading.Tasks; 
using Microsoft.AspNetCore.Builder; 
using Microsoft.AspNetCore.Http; 
using Microsoft.IdentityModel.Tokens; 
using OpenGameListWebApp.Data; 
using Microsoft.AspNetCore.Identity; 
using OpenGameListWebApp.Data.Users; 
using System.IdentityModel.Tokens.Jwt; 
using System.Security.Claims; 
using Newtonsoft.Json; 
 
namespace OpenGameListWebApp.Classes 
{ 
    public class JwtProvider 
    { 
        #region Private Members 
        private readonly RequestDelegate _next; 
 
        // JWT-related members 
        private TimeSpan TokenExpiration; 
        private SigningCredentials SigningCredentials; 
 
        // EF and Identity members, available through DI 
        private ApplicationDbContext DbContext; 
        private UserManager<ApplicationUser> UserManager; 
        private SignInManager<ApplicationUser> SignInManager; 
        #endregion Private Members 
 
        #region Static Members 
        private static readonly string PrivateKey = "private_key_1234567890"; 
        public static readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(PrivateKey)); 
        public static readonly string Issuer = "OpenGameListWebApp"; 
        public static string TokenEndPoint = "/api/connect/token"; 
        #endregion Static Members 
 
        #region Constructor 
        public JwtProvider( 
            RequestDelegate next,  
            ApplicationDbContext dbContext,  
            UserManager<ApplicationUser> userManager,  
            SignInManager<ApplicationUser> signInManager) 
        { 
            _next = next; 
 
            // Instantiate JWT-related members 
            TokenExpiration = TimeSpan.FromMinutes(10); 
            SigningCredentials =  new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256); 
 
            // Instantiate through Dependency Injection 
            DbContext = dbContext; 
            UserManager = userManager; 
            SignInManager = signInManager; 
        } 
        #endregion Constructor 
 
        #region Public Methods 
        public Task Invoke(HttpContext httpContext) 
        { 
            // Check if the request path matches our LoginPath 
            if (!httpContext.Request.Path.Equals(TokenEndPoint, StringComparison.Ordinal)) return _next(httpContext); 
 
            // Check if the current request is a valid POST with the appropriate content type (application/x-www-form-urlencoded) 
            if (httpContext.Request.Method.Equals("POST") && httpContext.Request.HasFormContentType) 
            { 
                // OK: generate token and send it via a json-formatted string 
                return CreateToken(httpContext); 
            } 
            else 
            { 
                // Not OK: output a 400 - Bad request HTTP error. 
                httpContext.Response.StatusCode = 400; 
                return httpContext.Response.WriteAsync("Bad request."); 
            } 
        } 
        #endregion Public Methods 
 
        #region Private Methods 
        private async Task CreateToken(HttpContext httpContext) 
        { 
            try 
            { 
                // retrieve the relevant FORM data 
                string username = httpContext.Request.Form["username"]; 
                string password = httpContext.Request.Form["password"]; 
 
                // check if there's an user with the given username 
                var user = await UserManager.FindByNameAsync(username); 
                // fallback to support e-mail address instead of username 
                if (user == null && username.Contains("@")) user = await UserManager.FindByEmailAsync(username); 
 
                var success = user != null && await UserManager.CheckPasswordAsync(user, password); 
                if (success) 
                { 
                    DateTime now = DateTime.UtcNow; 
 
                    // add the registered claims for JWT (RFC7519). 
                    // For more info, see https://tools.ietf.org/html/rfc7519#section-4.1 
                    var claims = new[] { 
                        new Claim(JwtRegisteredClaimNames.Iss, Issuer), 
                        new Claim(JwtRegisteredClaimNames.Sub, username), 
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 
                        new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) 
                        // TODO: add additional claims here 
                    }; 
 
                    // Create the JWT and write it to a string 
                    var token = new JwtSecurityToken( 
                        claims: claims, 
                        notBefore: now, 
                        expires: now.Add(TokenExpiration), 
                        signingCredentials: SigningCredentials); 
                    var encodedToken = new JwtSecurityTokenHandler().WriteToken(token); 
 
                    // build the json response 
                    var jwt = new { 
                        access_token = encodedToken, 
                        expiration = (int)TokenExpiration.TotalSeconds 
                    }; 
 
                    // return token 
                    httpContext.Response.ContentType = "application/json"; 
                    await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(jwt)); 
                    return; 
                } 
            } 
            catch (Exception ex) 
            { 
                // TODO: handle errors 
            } 
 
            httpContext.Response.StatusCode = 400; 
            await httpContext.Response.WriteAsync("Invalid username or password."); 
        } 
        #endregion Private Methods 
    } 
 
    #region Extension Methods 
    // Extension method used to add the middleware to the HTTP request pipeline. 
    public static class JwtProviderExtensions 
    { 
        public static IApplicationBuilder UseJwtProvider(this IApplicationBuilder builder) 
        { 
            return builder.UseMiddleware<JwtProvider>(); 
        } 
    } 
    #endregion Extension Methods 
} 

Adding the middleware to the pipeline

Now that we have created our JwtProvider middleware, we can add it to the request pipeline together with the built-in JwtBearerMiddleware. In order to do that, open the Startup.cs file and add the following code to the Configure method (new lines highlighted):

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, DbSeeder dbSeeder) 
{ 
    loggerFactory.AddConsole(Configuration.GetSection("Logging")); 
    loggerFactory.AddDebug(); 
 
    // Configure a rewrite rule to auto-lookup for standard default files such as index.html. 
    app.UseDefaultFiles(); 
 
    // Serve static files (html, css, js, images & more). See also the following URL: 
    // https://docs.asp.net/en/latest/fundamentals/static-files.html for further reference. 
    app.UseStaticFiles(new StaticFileOptions() 
    { 
        OnPrepareResponse = (context) => 
        { 
            // Disable caching for all static files. 
            context.Context.Response.Headers["Cache-Control"] = Configuration["StaticFiles:Headers:Cache-Control"]; 
            context.Context.Response.Headers["Pragma"] = Configuration["StaticFiles:Headers:Pragma"]; 
            context.Context.Response.Headers["Expires"] = Configuration["StaticFiles:Headers:Expires"]; 
        } 
    }); 
 
    // Add a custom Jwt Provider to generate Tokens

    app.UseJwtProvider();

    // Add the Jwt Bearer Header Authentication to validate Tokens

    app.UseJwtBearerAuthentication(new JwtBearerOptions()

    {

        AutomaticAuthenticate = true,

        AutomaticChallenge = true,

        RequireHttpsMetadata = false,

        TokenValidationParameters = new TokenValidationParameters()

        {

            IssuerSigningKey = JwtProvider.SecurityKey,

            ValidateIssuerSigningKey = true,

            ValidIssuer = JwtProvider.Issuer,

            ValidateIssuer = false,

            ValidateAudience = false

        }

    }); 
 
    // Add MVC to the pipeline 
    app.UseMvc(); 
 
    // TinyMapper binding configuration 
    TinyMapper.Bind<Item, ItemViewModel>(); 
 
    // Seed the Database (if needed) 
    try 
    { 
        dbSeeder.SeedAsync().Wait(); 
    } 
    catch (AggregateException e) 
    { 
        throw new Exception(e.ToString()); 
    } 
} 

To avoid compilation errors, be sure to declare the following namespaces to the beginning of the file:

using OpenGameListWebApp.Classes; 
using Microsoft.IdentityModel.Tokens; 

It's important to focus on two important things here:

  • Middleware order does indeed count. Notice how MVC gets added after JwtProvider and JwtBearerAuthentication, so the MVC default routing strategies won't interfere with them.
  • There's no AspNetCore.Identity middleware in there. We purposely avoided calling the app.UseIdentity() extension because it internally wraps app.UseCookieAuthentication(), which is something we don't need. We might want to add it if we want to support cookies over headers, or even use both of them.

Tip

To know more about what's under the hood of app.UseIdentity(), it can be useful to take a look at the extension's source code, which is publicly available on GitHub at the following URL:

https://github.com/aspnet/Identity/blob/dev/src/Microsoft.AspNetCore.Identity/BuilderExtensions.cs.

With this, we're done with the server-side part of our job. Let's switch to the client side.

Angular 2 login form

Remember that /Scripts/app/login.component.ts sample we created back in Chapter 3, Angular 2 Components and Client-Side Routing. The time has come to update it into a proper login form.

Open that file and modify the existing, almost empty template with the following code:

<div class="login-container"> 
    <h2 class="form-login-heading">Login</h2> 
    <div class="alert alert-danger" role="alert" *ngIf="loginError"> 
        <strong>Warning:</strong> Username or Password mismatch 
    </div> 
    <form class="form-login" [formGroup]="loginForm" (submit)="performLogin($event)"> 
        <input formControlName="username" type="text" class="form-control" placeholder="Your username or e-mail address" required autofocus /> 
        <input formControlName="password" type="password" class="form-control" placeholder="Your password" required /> 
        <div class="checkbox"> 
            <label> 
                <input type="checkbox" value="remember-me"> 
                Remember me 
            </label> 
        </div> 
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> 
    </form> 
</div> 

That's a simple login form with some Bootstrap and custom classes. Notice that we also defined an ngFormModel and an event handler method called performLogin that will trigger on each submit. Both should be added within the component's class implementation in the following way (new lines highlighted):

export class LoginComponent { 
    title = "Login"; 
    loginForm = null;

    constructor(private fb: FormBuilder) {

        this.loginForm = fb.group({

            username: ["", Validators.required],

            password: ["", Validators.required]

        });

    }

    performLogin(e) {

        e.preventDefault();

        alert(JSON.stringify(this.loginForm.value));

    } 
} 

We're introducing two new classes here:

  • FormBuilder: This is a factory class for creating instances of type FormGroup, which is how Angular 2 handles model-driven (or reactive) forms, we'll say more regarding this topic in a short while.
  • Validators: Angular has three built-in form validations that can be applied using this class. These are Validators.required, Validators.minLength(n), and Validators.maxLength(n). The names are self-explanatory, so we'll just say that we're using the first one, at least for now.

In order to use these classes, we need to add the following import statement at the beginning of the file:

import {FormBuilder, Validators} from "@angular/forms"; 

As we can see, there's also a performLogin method that we didn't implement much. We're just opening a UI alert to ensure us that everything is working so far, then bring the user back to our welcome view.

While we're here, let's take the chance to also add the Router component, so we'll be able to send the user somewhere right after the login. We can easily do that using the same DI technique we've already used a number of times.

This is how the login.component.ts will look after these changes:

import {Component} from "@angular/core"; 
import {FormBuilder, Validators} from "@angular/forms"; 
import {Router} from "@angular/router"; 
 
@Component({ 
    selector: "login", 
    template: ` 
    <div class="login-container"> 
      <h2 class="form-login-heading">Login</h2> 
      <form class="form-login" [ngFormModel]="loginForm" (submit)="performLogin($event)"> 
        <input ngControl="username" type="text" class="form-control" placeholder="Your username or e-mail address" required autofocus /> 
        <input ngControl="password" type="password" class="form-control" placeholder="Your password" required /> 
        <div class="checkbox"> 
          <label> 
            <input type="checkbox" value="remember-me"> Remember me 
          </label> 
        </div> 
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> 
      </form> 
    </div> 
    ` 
}) 
 
export class LoginComponent { 
    title = "Login"; 
    loginForm = null; 
 
    constructor( 
        private fb: FormBuilder,  
        private router: Router) { 
        this.loginForm = fb.group({ 
            username: ["", Validators.required], 
            password: ["", Validators.required] 
        }); 
    } 
 
    performLogin(e) { 
        e.preventDefault(); 
        alert(JSON.stringify(this.loginForm.value)); 
    } 
} 
 

Adding styles

As for the custom CSS classes, we can add them to our Scripts/less/style.less file:

.login-container { 
    max-width: 330px; 
    padding: 15px; 
    .form-login { 
        margin: 0 0 10px 20px; 
        .checkbox { 
            margin-bottom: 10px; 
        } 
        input { 
            margin-bottom: 10px; 
        } 
    } 
} 

Updating the root module file

Our renewed LoginComponent should compile just fine. However, if we try to run the app now, we would get a full-scale Angular 2 runtime error in the browser's console log:

Updating the root module file

Pretty scary, isn't it?

When we see something like that in Angular 2, it usually means that we're missing a required module. That's exactly the case. In order to use reactive forms classes, we need to open our /Scripts/app/app.module.ts file and append ReactiveFormsModule to the following existing import statement, near the beginning of the file:

import {FormsModule, ReactiveFormsModule} from "@angular/forms"; 

And also add it to the imports array as follows:

imports: [ 
    BrowserModule, 
    HttpModule, 
    FormsModule, 
    ReactiveFormsModule, 
    RouterModule, 
    AppRouting 
], 

Once done, our application will be able to run without errors.

Wait a minute...FormsModule has been there since Chapter 3, Angular 2 Components and Client-Side Routing! On top of that, we even used it to build the ItemDetailEditComponent form, which happens to work just fine! Why do we need ReactiveFormsModule now?

As a matter of fact, we don't; we could stick to the FormsModule and build another template-driven form just like the one we already did. As a matter of fact, since this is a tutorial application, we took the chance to use the alternative strategy provided by Angular 2 to build forms: the model-driven (or reactive) forms approach.

This clarification raises a predictable question: which one of them is better? The answer is not easy, as both techniques have their advantages. To keep it extremely simple, we can say that template-driven forms are generally simpler to pull off, but they're rather difficult to test and validate as they become complex; conversely, model-driven forms do have an harder learning curve but they usually perform better when dealing with large forms, as they allow us to unit test their whole validation logic.

Note

We won't explore these topics further, as they would take us way beyond the scope of this book. For more info regarding template-driven and model-driven forms, we strongly suggest reading the following article from the Angular 2 developers blog:

http://blog.angular-university.io/introduction-to-angular-2-forms-template-driven-vs-model-driven/

And also check out the official Angular 2 documentation regarding forms:

https://angular.io/docs/ts/latest/guide/forms.html

UI and validation test

Let's do a quick test right now. Hit F5 and click on the Login top navigation bar. We should be welcomed by something like this:

UI and validation test

Let's now check the validators by hitting the Sign in button, leaving the input fields empty. We can see the two textboxes react accordingly, since they're both expecting a required value:

UI and validation test

Finally, let's test the outcome JSON by filling up the input fields with some random values and pressing the Sign in button again:

UI and validation test

That's it. It seems that our login form is working fine.

AuthService component

Now we need to create a dedicated service to handle the login and logout operations.

Right-click on the /Scripts/app/ folder, select Add | New Item and add a new auth.service.ts file to the project, then fill it with the following code:

import {Injectable, EventEmitter} from "@angular/core"; 
import {Http, Headers, Response, RequestOptions} from "@angular/http"; 
import {Observable} from "rxjs/Observable"; 
 
@Injectable() 
export class AuthService { 
    authKey = "auth"; 
 
    constructor(private http: Http) { 
    } 
 
    login(username: string, password: string): any { 
        var url = "api/connect/token";  // JwtProvider's LoginPath 
 
        var data = { 
            username: username, 
            password: password, 
            client_id: "OpenGameList", 
            // required when signing up with username/password 
            grant_type: "password", 
            // space-separated list of scopes for which the token is issued 
            scope: "offline_access profile email" 
        }; 
 
        return this.http.post( 
            url, 
            this.toUrlEncodedString(data), 
            new RequestOptions({ 
                headers: new Headers({ 
                    "Content-Type": "application/x-www-form-urlencoded" 
                }) 
            })) 
            .map(response => { 
                var auth = response.json(); 
                console.log("The following auth JSON object has been received:"); 
                console.log(auth); 
                this.setAuth(auth); 
                return auth; 
            }); 
    } 
 
    logout(): boolean { 
        this.setAuth(null); 
        return false; 
    } 
 
    // Converts a Json object to urlencoded format 
    toUrlEncodedString(data: any) { 
        var body = ""; 
        for (var key in data) { 
            if (body.length) { 
                body += "&"; 
            } 
            body += key + "="; 
            body += encodeURIComponent(data[key]); 
        } 
        return body; 
    } 
 
    // Persist auth into localStorage or removes it if a NULL argument is given 
    setAuth(auth: any): boolean { 
        if (auth) { 
            localStorage.setItem(this.authKey, JSON.stringify(auth)); 
        } 
        else { 
            localStorage.removeItem(this.authKey); 
        } 
        return true; 
    } 
 
    // Retrieves the auth JSON object (or NULL if none) 
    getAuth(): any { 
        var i = localStorage.getItem(this.authKey); 
        if (i) { 
            return JSON.parse(i); 
        } 
        else { 
            return null; 
        } 
    } 
 
    // Returns TRUE if the user is logged in, FALSE otherwise. 
    isLoggedIn(): boolean { 
        return localStorage.getItem(this.authKey) != null; 
    } 
} 

This code has some resemblance to the one we used in the item.service.ts class. This can be expected, since both are Angular 2 service-type components used to instantiate service accessor objects, with the purpose of sending and receiving data to and from the web APIs. However, there are some key differences that might be worthy of attention:

  • The content-type set for the Login method's POST request has been set to application/x-www-form-urlencoded instead of application/json to comply with the requirements set in the JwtProvider class.
  • We store the result locally by making use of the localStorage object, which is part of HTML5's Web Storage API. This is a local caching object that keeps its content with no given expiration date. That's a great way to store our JWT-related JSON response, as we want to keep it even when the browser is closed. Before doing that, we choose to convert it into a string using JSON.stringify, since not all localStorage browser implementations can store JSON-type objects flawlessly.

Tip

Alternatively, in case we were to delete the token whenever the user closes the specific browser tab, we could use the sessionStorage object, which stores data only until the currently active session ends.

It's worth noting that we defined three methods to handle localStorage: setAuth(), getAuth(), and isLoggedIn(). The first one is in charge of insert, update, and delete operations; the second will retrieve the auth JSON object (if any); and the last one can be used to check whether the current user is authenticated or not, without having to JSON.parse it.

Updating the AppModule

In order to test our new AuthService component, we need to hook it up to the AppModule and to the LoginComponent we created a short while ago.

Open the /Scripts/app/app.module.ts file and add the following import line between the AppRouting and ItemService lines:

import {AppRouting} from "./app.routing"; 
import {AuthService} from "./auth.service"; 
import {ItemService} from "./item.service"; 

Then scroll down to the providers array and add it there too:

providers: [ 
    AuthService, 
    ItemService 
], 

Updating the LoginComponent

Once done, switch back to the Scripts/app/login.component.ts file and replace the content of the source code as follows (new/updated lines are highlighted):

import {Component} from "@angular/core"; 
import {FormBuilder, Validators} from "@angular/forms"; 
import {Router} from "@angular/router"; 
import {AuthService} from "./auth.service"; 
 
@Component({ 
    selector: "login", 
    template: ` 
    <div class="login-container"> 
      <h2 class="form-login-heading">Login</h2> 
      <div class="alert alert-danger" role="alert" *ngIf="loginError"><strong>Warning:</strong> Username or Password mismatch</div> 
      <form class="form-login" [formGroup]="loginForm" (submit)="performLogin($event)"> 
        <input formControlName="username" type="text" class="form-control" placeholder="Your username or e-mail address" required autofocus /> 
        <input formControlName="password" type="password" class="form-control" placeholder="Your password" required /> 
        <div class="checkbox"> 
          <label> 
            <input type="checkbox" value="remember-me"> 
            Remember me 
          </label> 
        </div> 
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> 
      </form> 
    </div> 
    ` 
}) 
 
export class LoginComponent { 
    title = "Login"; 
    loginForm = null; 
    loginError = false; 
 
    constructor( 
        private fb: FormBuilder, 

private router: Router,
  private authService: AuthService) { 
        this.loginForm = fb.group({ 
            username: ["", Validators.required], 
            password: ["", Validators.required] 
        }); 
    } 
 
    performLogin(e) { 
        e.preventDefault(); 
        var username = this.loginForm.value.username;

        var password = this.loginForm.value.password;

        this.authService.login(username, password)

            .subscribe((data) => {

                // login successful

                this.loginError = false;

                var auth = this.authService.getAuth();

                alert("Our Token is: " + auth.access_token);

                this.router.navigate([""]);

            },

            (err) => {

                console.log(err);

                // login failure

                this.loginError = true;

            }); 
    } 
} 

What we did here is pretty straightforward:

  • We included the import reference for our AuthService component and added it to the constructor, so we can have it available using DI.
  • We added a new loginError local variable that will reflect the outcome of the last login attempt.
  • We added a <div> element acting as an alert, to be shown whenever the loginError becomes true.
  • We modified the performLogin method to make it send the username and password values to the AuthService component's login method, so it can perform the following tasks:
    • Issue an HTTP request to the JwtProvider middleware
    • Receive a valid JWT accordingly and persist it into the localStorage object cache
    • Return true in case of success or false in case of failure
    • If everything goes as expected, we'll be shown a confirmation popup alert and route the user back to the welcome view; otherwise, we'll show the wrong username or password alert above the form

Login test

Let's run a quick test to see whether everything is working as expected. Hit F5, then navigate through the login view using the top navigation menu. Once there, fill in the login form with some incorrect data to test the Wrong Username or Password alert and left-click on the Sign in button:

Login test

Now, let's test a successful login attempt by filling in the form again, this time using the actual Admin user credentials as defined within the DbSeeder class:

  • E-mail: Admin
  • Password: Pass4Admin

Then, left-click on the Sign in button.

If everything has been set up properly, we should receive the following response:

Login test

If we see something like this, it means that our JwtProvider works!

All we need to do now is to find a way to put that token inside the headers of all our subsequent requests, so we can check the token validation as well and complete our authentication cycle.

AuthHttp wrapper

A rather easy way to do that with Angular 2 is create a wrapper class that will internally use the standard Http component right after having it configured to suit our needs.

Right-click on the /Scripts/app/ folder, then select Add | New Item. Add a new auth.http.ts file to the project and fill it with the following code:

import {Injectable} from '@angular/core'; 
import {Http, Headers} from '@angular/http'; 
 
@Injectable() 
export class AuthHttp { 
    http = null; 
    authKey = "auth"; 
 
    constructor(http: Http) { 
        this.http = http; 
    } 
 
    get(url, opts = {}) { 
        this.configureAuth(opts); 
        return this.http.get(url, opts); 
    } 
 
    post(url, data, opts = {}) { 
        this.configureAuth(opts); 
        return this.http.post(url, data, opts); 
    } 
 
    put(url, data, opts = {}) { 
        this.configureAuth(opts); 
        return this.http.put(url, data, opts); 
    } 
 
    delete(url, opts = {}) { 
        this.configureAuth(opts); 
        return this.http.delete(url, opts); 
    } 
 
    configureAuth(opts: any) { 
        var i = localStorage.getItem(this.authKey); 
        if (i != null) { 
            var auth = JSON.parse(i); 
            console.log(auth); 
            if (auth.access_token != null) { 
                if (opts.headers == null) { 
                    opts.headers = new Headers(); 
                } 
                opts.headers.set("Authorization", `Bearer ${auth.access_token}`); 
            } 
        } 
    } 
} 

There's not much to say here, it's just a wrapper that calls the configureAuth method internally to add the JWT token stored in the browser's localStorage ,if any, to each request's headers.

Since we'll be using the AuthHttp wrapper anywhere in our application, the first thing we need to do is add it to the application's root module, just like we did with the AuthService a short while ago. Open the Scripts/app/app.module.ts file and add the usual import line between AppRouting and AuthService:

import {AppRouting} from "./app.routing"; 
import {AuthHttp} from "./auth.http"; 
import {AuthService} from "./auth.service"; 

And also add it to the providers array as follows:

providers: [ 
    AuthHttp, 
    AuthService, 
    ItemService 
], 

Now we can update each and every Http reference included in our other Angular 2 files and replace them with AuthHttp. As we can easily guess, the affected components are the two service classes we're using to connect through the web API interface: auth.service.ts and item.service.ts.

For both of them, we need to add the following line at the beginning of the file:

import {AuthHttp} from "./auth.http"; 

And change the constructor parameters in the following way:

constructor(private http: AuthHttp) {  

Adding authorization rules

It's time to see whether our manual JWT-based auth implementation is working as expected. Before doing that, though, we need to define some testable navigation patterns that will allow us to differentiate the logged-in user from the anonymous one. It's actually easy to do that, since we already have some content that should be made accessible to authenticated users only. We need to handle them on the client side and also on the server side.

Adapting the client

Let's start by updating the main menu navigation bar. Open the Scripts/app/app.component.ts file and add the following import reference near the top:

import {AuthService} from "./auth.service";

Right after that, change the template section in the following way (new/updated lines are highlighted):

<nav class="navbar navbar-default navbar-fixed-top"> 
    <div class="container-fluid"> 
        <input type="checkbox" id="navbar-toggle-cbox"> 
        <div class="navbar-header"> 
            <label for="navbar-toggle-cbox" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> 
                <span class="sr-only">Toggle navigation</span> 
                <span class="icon-bar"></span> 
                <span class="icon-bar"></span> 
                <span class="icon-bar"></span> 
            </label> 
            <a class="navbar-brand" href="javascript:void(0)"> 
                <img alt="logo" src="/img/logo.svg" /> 
            </a> 
        </div> 
        <div class="collapse navbar-collapse" id="navbar"> 
            <ul class="nav navbar-nav"> 
                <li [class.active]="isActive([''])"> 
                    <a class="home" [routerLink]="['']">Home</a> 
                </li> 
                <li [class.active]="isActive(['about'])"> 
                    <a class="about" [routerLink]="['about']">About</a> 
                </li> 
                <li *ngIf="!authService.isLoggedIn()"
               [class.active]="isActive(['login'])">

                    <a class="login" [routerLink]="['login']">Login</a>
 
               </li>

                <li *ngIf="authService.isLoggedIn()">

                    <a class="logout" href="javascript:void(0)"
                    (click)="logout()">Logout</a>

                </li>

                <li *ngIf="authService.isLoggedIn()"
                [class.active]="isActive(['item/edit', 0])">

                    <a class="add" [routerLink]="['item/edit', 0]">Add New</a>

                </li> 
            </ul> 
        </div> 
    </div> 
</nav> 
<h1 class="header">{{title}}</h1> 
<div class="main-container"> 
    <router-outlet></router-outlet> 
</div> 

What we did here is pretty easy to understand:

  • We added a ngIfbuilt-in directive tothe Login menu element, since we don't want it to appear if the user is already logged in.
  • We also added another Logout menu element with similar yet opposing behavior, as we don't want it to be seen if the user is not logged in. Clicking on this element will trigger the logout() method, which we'll be adding shortly.
  • We added another ngIf condition to the New Item menu element, as it should be seen by logged-in users only.

In order to use the authService object, we also need to instantiate it through dependency injection within the class constructor, which is another thing we have to change (new/updated lines highlighted):

constructor(public router: Router, public authService: AuthService) { } 

Finally, we need to implement that logout() method we talked about earlier:

logout(): boolean { 
    // logs out the user, then redirects him to Welcome View. 
    if (this.authService.logout()) { 
        this.router.navigate([""]); 
    } 
    return false; 
} 

Nothing odd here, just a standard logout and redirect behavior to adopt when the user chooses to perform a logout.

The changes we applied to the AppComponent template should also be performed in the ItemDetailViewComponent templates as well. Open Scripts/app/item-detail-view.component.ts and add the import line:

import {AuthService} from "./auth.service"; 

Then move to the constructor and add the AuthService reference there for DI (new code highlighted):

constructor( 
    private authService: AuthService, 
    private itemService: ItemService, 
    private router: Router, 
    private activatedRoute: ActivatedRoute) { } 

And finally, update the template section accordingly, using the same ngIf built-in directive we used before to show/hide the Edit tab accordingly to the current user's logged in status:

<li *ngIf="authService.isLoggedIn()" role="presentation"> 
    <a href="javascript:void(0)" (click)="onItemDetailEdit(item)">Edit</a> 
</li> 

Testing the client

Let's hit F5 and see whether everything is working as it should. We should start as anonymous users and see something like this:

Testing the client

We can see that the New Item menu element is gone. That's expected; we're not logged in, so we shouldn't be able to add a new item.

From there, we can click the Login menu element and be brought to the login view, where we can input the admin credentials (admin/pass4admin, in case we forgot). As soon as we hit the Sign In button, we will be routed back to the welcome view, where we should be greeted by something like the following screenshot:

Testing the client

The Login menu element is gone, replaced by Logout and Add New. We can then click on Logout and see both of them replaced by the former again.

So far, so good. However, we're not done with the client yet. These modifications prevent the user from clicking some links they're not allowed to see, yet they are unable to stop the user from going to their given destinations. For example, the user could manually input the routes within the browser's navigation bar and go to the login view while being already logged in, or even worse access the add/edit item view despite being anonymous.

In order to avoid that, we can add a login status check within the login.component.ts constructor (new lines highlighted):

constructor( 
    private fb: FormBuilder, 
    private router: Router, 
    private authService: AuthService) { 
    if (this.authService.isLoggedIn()) {

        this.router.navigate([""]);

    } 
    this.loginForm = fb.group({ 
        username: ["", Validators.required], 
        password: ["", Validators.required] 
    }); 
} 

Also add it to the ngOnInit startup method within the item-detail-edit.component.ts file:

ngOnInit() { 
    if (!this.authService.isLoggedIn()) {

        this.router.navigate([""]);

    } 
    var id = +this.activatedRoute.snapshot.params["id"]; 
    if (id) { 
        this.itemService.get(id).subscribe( 
            item => this.item = item 
        ); 
    } 
    else if (id === 0) { 
        console.log("id is 0: adding a new item..."); 
        this.item = new Item(0, "New Item", null); 
    } 
    else { 
        console.log("Invalid id: routing back to home..."); 
        this.router.navigate([""]); 
    } 
} 

Doing this will also require adding the corresponding import reference line near the topmost section of the item-detail-edit.component.ts file:

import {AuthService} from "./auth.service"; 

And the DI injection in the constructor method:

constructor( 
    private authService: AuthService, 
    private itemService: ItemService, 
    private router: Router, 
    private activatedRoute: ActivatedRoute) { } 

That way, any unauthorized user will be bounced back whenever they try to manually hack our route mechanism by issuing a direct request to these views.

Protecting the server

Now that our client is more or less ready, it's time to shield our web API interface from unauthorized requests as well. We can easily do that using the [Authorize] attribute, which can be used to restrict access to any controller and/or controller method we don't want to open to unauthorized access.

To implement the required authorization behavior, it could be wise to use it on the Add, Update, and Delete methods of our ItemsController class (new lines are highlighted):

[HttpPost()] 
[Authorize] 
public IActionResult Add([FromBody]ItemViewModel ivm) 
{ 
    [...] 
} 
 
[HttpPut("{id}")] 
[Authorize] 
public IActionResult Update(int id, [FromBody]ItemViewModel ivm) 
{ 
    [...] 
} 
 
[HttpDelete("{id}")] 
[Authorize] 
public IActionResult Delete(int id) 
{ 
    [...] 
} 

In order to use the [Authorize] attribute, we also need to declare the following namespace reference at the beginning of the file:

using Microsoft.AspNetCore.Authorization; 

Now these methods are protected against unauthorized access, as they will accept only requests coming from logged-in users/clients with a valid JWT token. Those who don't have it will receive a 401 - Unauthorized HTTP error response.

Retrieving the user ID

Before closing the ItemsController class file, we should take the chance to remove the item.UserId value override we defined back in Chapter 5, Persisting Changes, when we had no authentication mechanism in place:

// TODO: replace the following with the current user's id when authentication will be available. 
item.UserId = DbContext.Users.Where(u => u.UserName == "Admin").FirstOrDefault().Id; 

Now that we're working with real users, we definitely have to remove this ugly workaround and find a way to retrieve the actual user ID. Luckily enough, when we implemented our very own JWT provider earlier, we did actually put it in the claims JWT token (JwtProvider class, CreateToken method):

new Claim(JwtRegisteredClaimNames.Sub, user.Id), 

This means that we can retrieve it in the following way (updated code is highlighted):

item.UserId = User.FindFirst(ClaimTypes.NameIdentifier).Value;>

Let's perform this change and move on.

Note

This minor update should be enough for now. However, it won't work when dealing with external OpenId and/or OAuth2 providers, as they will put their own data in these claims. Retrieving our local UserId in such scenarios will require some additional work, such as querying a dedicated lookup table. We'll see more about this during Chapter 8, Third-Party Authentication and External Providers.

Authorization test

Before going further, it's definitely time to perform a client/server interaction test to ensure that our authorization pattern is working as expected.

From the Visual Studio source code editing interface, we can put a breakpoint right below the ItemsControllerAdd method:

Authorization test

Once done, we can hit F5, navigate from the welcome view to the login view, and authenticate ourselves. Right after that, we'll be able to click upon the Add New menu element.

From there, we can fill in the form with some random text and click on the Save button. The form will consequently call the Add method of ItemsController, hopefully triggering our breakpoint.

Open a Watch window (Debug | Windows | Watch | Watch 1) and check the HttpContext.User.Identity.IsAuthenticated property value:

Authorization test

If it's true, it means that we've been successfully authenticated. That shouldn't be surprising, since our request already managed to get inside a method protected by an [Authorize] attribute.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset