Click here to Skip to main content
13,867,447 members
Click here to Skip to main content
Add your own
alternative version

Stats

30.7K views
921 downloads
31 bookmarked
Posted 23 Feb 2017
Licenced CPOL

SPA^2 using ASP.Net Core 1.1 + Angular 2.4 - part 4

, 9 Mar 2017
Rate this:
Please Sign up or sign in to vote.
Add JSON Web Token Authentication (JWT) using OpenIDDict to our ASP.Net Core + Angular 2 SPA. Source includes VS2015 and VS2017 versions.

Source

If the above link does not work, you can get the source from Github here.

Introduction

My aim for these articles is to show another way to create a SPA using ASP.Net Core and Angular 2; this technique outlined in parts 1-3 allows you to keep the best features of each, to simplify development and inevitable changes in a project, remove the temptation for cut-paste coding and still keep things relatively simple.  Too often applications that use ASP.Net Core and Angular 2 together become a complex web of code with server side pre-rendering, webpack and loads of middleware "glue", or the other extreme treat them as completely separate islands using 'flat' HTML templates.

ASP.Net Core uses partial views to deliver smarter Angular 2 templates. Because these Angular 2 templates are generated by tag helpers and make use of your C# data model, they can accelerate your Angular development. Since the tag helpers are driven from your data model, they reducing repetitive and error-prone hand-coding, ensure your client side code is automatically generated (making it easier to update styles/classes, descriptions or add tool tips) and all of this leaves you more time for the pieces of your application that matter.

Part 1 - How to integrate ASP.Net Core and Angular 2
Part 2 - How to use Tag Helpers to display data
Part 3 - How to use Tag Helpers for data input, added SQL backend using EF Core.
this is part 4 - Adding token authentication with JWT using OpenIdDict. 

Background

Although security is not the primary aim of this series of articles, it is an important feature of most web applications we'll build, this part, part 4 will cover this topic. At the same time you 'll see an example of how our simple tag helpers make data entry and validation easier.

Token based security is one of the most common methods used to secure modern applications. We'll add authentication using JWT tokens, using the OpenIdDict package from Kévin Chalet

I have chosen OpenIdDict as the authentication library to use for this article since it is relatively easy to use, "light-weight", free, and has source and sample code available.  You can look inside the code and samples and learn a lot about token authentication.

There were many other choices, including the freemium 'cloud' based product Auth0  or  Identity Server 4 which you can host yourself. Your choice will depend on many factors; the level of support you need (commercial support vs open source), what clients will be connecting to your application (web, mobile, desktop, in house,  different domain/3rd party). Covering all of these considerations would easily fill a whole series, just on that topic.For further information on Token Validation, see this tutorial by Kevin Chalet.

UPDATE 2nd March 2017: If you are reading this for the first time you can probably ignore this, skip ahead to "Adding Authentication using OpenIdDict".

On the other hand, if you have already started using this code, and running into issues, I've just discovered the latest OpenIdDict package (version 1.0.0-rc1-1093) now requires a few changes that have not yet made it into the OpenIdDict sample. If you still have version 1.0.0-rc1-1077 (tell by looking inside the project.lock.json file) you can force the fault, and an update to the new version, by deleting the project.lock.json file, rebuilding the app, which will pull down the latest packages, and in turn cause an issue when trying to login.

Typically the error is a server side exception, or 500 error, saying something like:

An unhandled exception occurred while processing the request

InvalidOperationException: The authentication ticket was rejected because it doesn't contain the mandatory subject claim.

This version of the current article (below) has been updated with the changes needed to support the new version of OpenIdDict. These changes have also been made to the source on the github repo, in the branch part4.

The first change is in startup.cs where you need to change from this:

                // to replace the default OpenIddict entities.
    options.UseOpenIddict();
});
// Register the Identity services.
services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<A2spaContext>()
    .AddDefaultTokenProviders();
 
// Register the OpenIddict services.
services.AddOpenIddict()
    // Register the Entity Framework stores.

to this:

                // to replace the default OpenIddict entities.
    options.UseOpenIddict();
});
// Register the Identity services.
services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<A2spaContext>()
    .AddDefaultTokenProviders();
 
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
    options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
    options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
    //options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
 
// Register the OpenIddict services.
services.AddOpenIddict()
    // Register the Entity Framework stores.

The 3rd line of claims added was remarked out, as it the enum .Role is not yet supported in the OpenIdDict package. Reminder: OpenIdDict is a beta package, if you want stability look at Auth0 or wait until Identity Server 4 or OpenIdDict are out of Beta. If you are in development now, and willing to put up with breaking changes, perhaps stick with OpenIdDict. Up to you and your level of risk + time frames.

The next small change is to add this to your startup.cs usings:

using AspNet.Security.OpenIdConnect.Primitives;

and last change. update project.json from this: 

  "Microsoft.AspNetCore.Identity": "1.1.0",
  "AspNet.Security.OAuth.Introspection": "1.0.0-beta1-0201",
  "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0",
  "AspNet.Security.OAuth.Validation": "1.0.0-beta1-0201"
},

to this:

  "Microsoft.AspNetCore.Identity": "1.1.0",
  "AspNet.Security.OAuth.Introspection": "1.0.0-beta1-*",
  "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0",
  "AspNet.Security.OAuth.Validation": "1.0.0-beta1-*"
},

At this point, delete your project.lock.json file and rebuild, press Ctrl-F5 and logins should work once again.

(end of update)

Adding Authenticationusing OpenIdDict

OpenIDDict has been made available on GitHub primarily through Kévin Chalet  and (in his words) it is based on AspNet.Security.OpenIdConnect.Server (codenamed ASOS) .. it can control the OpenID Connect authentication flow and can be used with any membership stack, including ASP.NET Core Identity.

We will be using "password flow" where an end user enters username and password that are validated against a SQL database (could be almost any DB) using entity framework core.

If valid an encrypted 'token' is produced and passed back to the client web page in the response from the server.

Once the client this validation phase passed, there is no further need to revalidate with the username or password, as the 'token' is used by the client to prove who they are (authentication) and what they are allowed to do (authorisation). The token has embedded into it an expiry time which along with other encrypted data to help ensure that the token has not been tamered with.

When the browser receives this token, the Angular 2 code stores this in the browser's session storage. You can update the code to use local storage instead of session storage if you wish to make browser wide validation possible. This token is then sent with each server request in the request header, then this token is validated at the server for integrity, and against the user credentials and roles. The debate over using local storage/session storage vs using cookies that is beyond the scope of this article, please research this elsewhere if you wish. It might be noted that if your end user has disabled cookies, token validation as described here will still work, since it is unaffected by cookies being enabled or not.

I'm generally following the code sample from OpenIdDict Samples, for PasswordFlow here:
https://github.com/openiddict/openiddict-samples/tree/master/samples/PasswordFlow

You could clone this and the OpenIDDict Core package for reference to help understand how it works.

First modification in our existing code from part 3, this will be in the /ViewModels folder, we need to add a new folder called Accounts, where we'll add two new class files to describe the data models we'll be using for new user registration as then for login. First create LoginViewModel.cs and edit it to this:

using System.ComponentModel.DataAnnotations;
 
namespace A2SPA.ViewModels.Account
{
    public class LoginViewModel
    {
        [Required, RegularExpression(@"([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})", ErrorMessage = "Please enter a valid email address.")]
        [EmailAddress]
        [Display(Name = "Username", ShortName = "Email", Prompt = "Email Address (username)")]
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
 
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
    }
}

Next create RegisterViewModel.cs which will inherit some properties from our Login View Model; edit it to read this:

using System.ComponentModel.DataAnnotations;
 
namespace A2SPA.ViewModels.Account
{
    public class RegisterViewModel : LoginViewModel
    {
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password Verification")]
        public string VerifyPassword { get; set; }
    }
}

Next we'll add a few new packages, dependencies using NuGet, and some using MyGet.

Make sure you update your package sources to include a MyGet feed, using the key aspnet-contrib and address: 

https://www.myget.org/F/aspnet-contrib/api/v3/index.json

You can do this either in Visual Studio 2015, where it will be used globally, or you edit the nuget.config file for this solution instead. You can edit nuget.config directly, it should be located right beside the solution file.

Create one if it doesn't exist. This is what should be in it.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="NuGet" value="https://api.nuget.org/v3/index.json" />
    <add key="aspnet-contrib" value="https://www.myget.org/F/aspnet-contrib/api/v3/index.json" />
  </packageSources>
</configuration>

Previously we have used NuGet Package Manager (and we still could), but to demonstrate another method, we'll add these by directly editing the package.json file. ). These additions should be in the dependencies section, and if put at the end of the section, ensure you have an added comma on the previous line:

"OpenIddict": "1.0.0-*",
"OpenIddict.EntityFrameworkCore": "1.0.0-*",
"OpenIddict.Mvc": "1.0.0-*",
"Microsoft.AspNetCore.Identity": "1.1.0",
"AspNet.Security.OAuth.Introspection": "1.0.0-beta1-*",
"Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0",
"AspNet.Security.OAuth.Validation": "1.0.0-beta1-*"

BTW From VS2017 we're losing package.json with the return of a more "traditional" project file that will contain these dependencies.

Next we'll edit our startup.cs file to add support for OpenIDDict. This involves adding a couple of new dependencies to provide access to both ApplicationUser and IdentityRoles, and a number of small changes through the file to set up routes to OpenIdDict's web services. As the changes are interspersed through the file, to avoid chance of mistakes, I've included the complete startup.cs (with changes) below. To see what was modified you can compare these in Git history.

Here is the complete new version of startup.cs below:

using A2SPA.Data;
using A2SPA.Models;
using AspNet.Security.OpenIdConnect.Primitives;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using System.IO;
 
namespace A2SPA
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }
 
        public IConfigurationRoot Configuration { get; }
 
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();
 
            services.AddDbContext<A2spaContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
 
                // Register the entity sets needed by OpenIddict.
                // Note: use the generic overload if you need
                // to replace the default OpenIddict entities.
                options.UseOpenIddict();
            });
            // Register the Identity services.
            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<A2spaContext>()
                .AddDefaultTokenProviders();
 
            // Configure Identity to use the same JWT claims as OpenIddict instead
            // of the legacy WS-Federation claims it uses by default (ClaimTypes),
            // which saves you from doing the mapping in your authorization controller.
            services.Configure<IdentityOptions>(options =>
            {
                options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
                options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
                //options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
            });
 
            // Register the OpenIddict services.
            services.AddOpenIddict()
                // Register the Entity Framework stores.
                .AddEntityFrameworkCoreStores<A2spaContext>()
 
                // Register the ASP.NET Core MVC binder used by OpenIddict.
                // Note: if you don't call this method, you won't be able to
                // bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
                .AddMvcBinders()
 
                // Enable the token endpoint.
                .EnableTokenEndpoint("/connect/token")
 
                // Enable the password flow.
                .AllowPasswordFlow()
 
                // During development, you can disable the HTTPS requirement.
                .DisableHttpsRequirement();
        }
 
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, A2spaContext context)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();
 
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
 
            // Add a middleware used to validate access
            // tokens and protect the API endpoints.
            app.UseOAuthValidation();
 
            // Alternatively, you can also use the introspection middleware.
            // Using it is recommended if your resource server is in a
            // different application/separated from the authorization server.
            //
            // app.UseOAuthIntrospection(options =>
            // {
            //     options.AutomaticAuthenticate = true;
            //     options.AutomaticChallenge = true;
            //     options.Authority = "http://localhost:58795/";
            //     options.Audiences.Add("resource_server");
            //     options.ClientId = "resource_server";
            //     options.ClientSecret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd";
            // });
 
            app.UseOpenIddict();
            app.UseDefaultFiles();
            app.UseStaticFiles();
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "node_modules")),
                RequestPath = "/node_modules"
            });
 
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
 
                // in case multiple SPAs required.
                routes.MapSpaFallbackRoute("spa-fallback", new { controller = "home", action = "index" });
 
            });
 
            if (env.IsDevelopment())
            {
                DbInitializer.Initialize(context);
            }
        }
    }
}

Next add a new partial view called _LoginPartial.cshtml into the /Views/Shared folder.

The code we'll add to this could have been added into AppComponent.cshtml directly, but separating this here demonstrates another use of server side shared partial views, where views can contain other partial views. This technique can be used to simplify large blocks of server side code and also to reduce repetition, as it allows simple reuse across other view pages.

This new partial view _LoginPartial.cshtml should be edited to contain this:

<div [hidden]="!isLoggedIn()">
    <ul class="nav navbar-nav navbar-right">
        <li>
            <a class="nav-link" (click)="logout()" routerLink="/home">Logout</a>
        </li>
    </ul>
</div>
 
<div [hidden]="isLoggedIn()">
    <ul class="nav navbar-nav navbar-right">
        <li><a class="nav-link" (click)="setTitle('Register - A2SPA')" routerLink="/register">Register</a></li>
        <li><a class="nav-link" (click)="setTitle('Login - A2SPA')" routerLink="/login">Login</a></li>
    </ul>
</div>

To use this new shared partial view reference it in the view AppComponent.cshtml as shown below. This short code block only shows the new updated menu section of the AppComponent.cshtlml view:

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li>
            <a class="nav-link" (click)="setTitle('Home - A2SPA')" routerLink="/home" routerLinkActive="active">Home</a>
        </li>
        <li [hidden]="!isLoggedIn()">
            <a class="nav-link" (click)="setTitle('About - A2SPA')" routerLink="/about">About</a>
        </li>
        <li>
            <a class="nav-link" (click)="setTitle('Contact - A2SPA')" routerLink="/contact">Contact</a>
        </li>
    </ul>
    @await Html.PartialAsync("_LoginPartial")
</div>

You'll notice some other changes to the menu. Soon we will no longer have access to the original About page without logging in. I've tried to keep the menu similar to the off-the-shelf VS2015 ASP.Net Core template.

We'll next add two new views; one for new user registration, the other a login page for users who have already registered.

In the  /Views/Partial folder add a new view called RegisterComponent.cshtml which will be used as an MVC partial view just like our existing About or Contact views. Edit this new view to contain the following:

@using A2SPA.ViewModels
@using A2SPA.ViewModels.Account
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *,A2SPA
@model RegisterViewModel
 
<div class="jumbotron center-block">
    <h2>Register</h2>
 
    <form role="form" #testForm="ngForm">
        <div *ngIf="registerViewModel != null">
            <tag-di for="Email"></tag-di>
            <tag-di for="Password"></tag-di>
            <tag-di for="VerifyPassword"></tag-di>
            <button type="button" (click)="register($event)" class="btn btn-default">Submit</button>
            <span class="small">Already registered? <a [routerLink]="['/login']"> Click here to Login</a></span>
        </div>
    </form>
</div>
 
<div *ngIf="errorMessage != null">
    <p>Error:</p>
    <pre>{{ errorMessage  }}</pre>
</div>

Next also in the  /Views/Partial folder add a new partial view called LoginComponent.cshtml and edit it to contain the following:

@using A2SPA.ViewModels
@using A2SPA.ViewModels.Account
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *,A2SPA
@model LoginViewModel
 
<div class="jumbotron center-block">
    <h2>Login</h2>
 
    <form role="form" #testForm="ngForm">
        <div *ngIf="loginViewModel != null">
            <tag-di for="Email"></tag-di>
            <tag-di for="Password"></tag-di>
            <button type="button" (click)="login($event)" class="btn btn-default">Submit</button>
            <span class="small">Not registered? <a [routerLink]="['/register']"> Click here to Register</a></span>
        </div>
    </form>
</div>
 
 
<div *ngIf="errorMessage != null">
    <p>Error:</p>
    <pre>{{ errorMessage  }}</pre>
</div>

Both of these much shorter that they might have been, thank you to tag helpers!

To allow access to these two new partial views, edit /Controllers/PartialController.cs to add these lines:

public IActionResult LoginComponent() => PartialView();
 
public IActionResult RegisterComponent() => PartialView();

Next create a /Models folder at the root level of the project, alongside /ViewModels. Inside this create a new class ApplicationUser.cs - this should be edited to contain the following code:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
 
namespace A2SPA.Models
{
    // Add profile data for application users by adding properties to the ApplicationUser class
    public class ApplicationUser : IdentityUser { }
}

As the comments suggest, you could use this extension of ApplicationUser to the existing IdentityUser class to support a more complex user profile, simply by adding the properties here.

We're next going to edit the context class /data/A2spaContext.cs to support the tables required by OpenIdDict. This will change A2spaContext to inherit from IdentityDbContext<ApplicationUser> instead of DbContext, then we'll call back into the base class to build our database from the data model at the end.

For clarity here is the complete new version of code from the A2spaContext.cs file:

using A2SPA.Models;
using A2SPA.ViewModels;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
 
namespace A2SPA.Data
{
    public class A2spaContext : IdentityDbContext<ApplicationUser>
    {
        public A2spaContext(DbContextOptions<A2spaContext> options) : base(options)
        {
        }
 
        public DbSet<TestData> TestData { get; set; }
 
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TestData>().ToTable("TestData");
            base.OnModelCreating(modelBuilder);
        }
    }
}

We need to update our existing SampleDataController.cs file to make sure it is now unavailable to anonymous users, and only usable by authenticated users. Add this using statement:

using Microsoft.AspNetCore.Authorization;

And then add the [Authorize] attribute here, as shown:

namespace A2SPA.Api
{
    [Authorize]
    [Route("api/[controller]")]

    public class SampleDataController : Controller

You could use the [Anonymous] attribute to open one or two methods if you wanted, but this simple addition will ensure all methods in the controller are unavailable to any users who have not logged on.

Almost to the end of our backend changes, we now need to add our new Web API controllers to support new user registration as well as login and logout methods. First in our /Api folder add the new class file AccountController.cs and then edit it to contain the following :

using A2SPA.Data;
using A2SPA.Models;
using A2SPA.ViewModels.Account;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
 
namespace A2SPA.Api
{
    [Authorize]
    public class AccountController : Controller
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly A2spaContext _context;
        private static bool _databaseChecked;
 
        public AccountController(UserManager<ApplicationUser> userManager, A2spaContext applicationDbContext)
        {
            _userManager = userManager;
            _context = applicationDbContext;
        }
 
        //
        // POST: /Account/Register
        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> Register([FromBody] RegisterViewModel model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
                var result = await _userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    return Ok();
                }
                AddErrors(result);
            }
 
            // If we got this far, something failed.
            return BadRequest(ModelState);
        }
 
        #region Helpers
 
        private void AddErrors(IdentityResult result)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }
        }
 
        #endregion
    }
}

The above AccountController was modified from the original, to remove database generation here, as we've incorporated this where it is already in startup.cs for our sample data. You could of course maintain two different data contexts; one just for data and the other for authentication.  For further information on this and the other OpenIdDict specific classes and code blocks, again please refer to the OpenIdDict core and sample sources.

The next new controller is also in the /Api folder and should be called AuthorizationController.cs and needs to be changed to this:

/*
 * Licensed under the Apache License, Version 2.0 (<a href="http://www.apache.org/licenses/LICENSE-2.0">http://www.apache.org/licenses/LICENSE-2.0</a>)
 * See <a href="https://github.com/openiddict/openiddict-core">https://github.com/openiddict/openiddict-core</a> for more information concerning
 * the license and the contributors participating to this project.
 */
 
using A2SPA.Models;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Core;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
 
namespace A2SPA.Api
{
    public class AuthorizationController : Controller
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
 
        public AuthorizationController(
            SignInManager<ApplicationUser> signInManager,
            UserManager<ApplicationUser> userManager)
        {
            _signInManager = signInManager;
            _userManager = userManager;
        }
 
        [HttpPost("~/connect/token"), Produces("application/json")]
        public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
        {
            Debug.Assert(request.IsTokenRequest(),
                "The OpenIddict binder for ASP.NET Core MVC is not registered. " +
                "Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
 
            if (request.IsPasswordGrantType())
            {
                var user = await _userManager.FindByNameAsync(request.Username);
                if (user == null)
                {
                    return BadRequest(new OpenIdConnectResponse
                    {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "The username/password couple is invalid."
                    });
                }
 
                // Ensure the user is allowed to sign in.
                if (!await _signInManager.CanSignInAsync(user))
                {
                    return BadRequest(new OpenIdConnectResponse
                    {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "The specified user is not allowed to sign in."
                    });
                }
 
                // Reject the token request if two-factor authentication has been enabled by the user.
                if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user))
                {
                    return BadRequest(new OpenIdConnectResponse
                    {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "The specified user is not allowed to sign in."
                    });
                }
 
                // Ensure the user is not already locked out.
                if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
                {
                    return BadRequest(new OpenIdConnectResponse
                    {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "The username/password couple is invalid."
                    });
                }
 
                // Ensure the password is valid.
                if (!await _userManager.CheckPasswordAsync(user, request.Password))
                {
                    if (_userManager.SupportsUserLockout)
                    {
                        await _userManager.AccessFailedAsync(user);
                    }
 
                    return BadRequest(new OpenIdConnectResponse
                    {
                        Error = OpenIdConnectConstants.Errors.InvalidGrant,
                        ErrorDescription = "The username/password couple is invalid."
                    });
                }
 
                if (_userManager.SupportsUserLockout)
                {
                    await _userManager.ResetAccessFailedCountAsync(user);
                }
 
                // Create a new authentication ticket.
                var ticket = await CreateTicketAsync(request, user);
 
                return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
            }
 
            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
                ErrorDescription = "The specified grant type is not supported."
            });
        }
 
        private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user)
        {
            // Create a new ClaimsPrincipal containing the claims that
            // will be used to create an id_token, a token or a code.
            var principal = await _signInManager.CreateUserPrincipalAsync(user);
 
            // Note: by default, claims are NOT automatically included in the access and identity tokens.
            // To allow OpenIddict to serialize them, you must attach them a destination, that specifies
            // whether they should be included in access tokens, in identity tokens or in both.
 
            foreach (var claim in principal.Claims)
            {
                // In this sample, every claim is serialized in both the access and the identity tokens.
                // In a real world application, you'd probably want to exclude confidential claims
                // or apply a claims policy based on the scopes requested by the client application.
                claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
                                      OpenIdConnectConstants.Destinations.IdentityToken);
            }
 
            // Create a new authentication ticket holding the user identity.
            var ticket = new AuthenticationTicket(
                principal, new AuthenticationProperties(),
                OpenIdConnectServerDefaults.AuthenticationScheme);
 
            // Set the list of scopes granted to the client application.
            // Note: the offline_access scope must be granted
            // to allow OpenIddict to return a refresh token.
            ticket.SetScopes(new[]
            {
                OpenIdConnectConstants.Scopes.OpenId,
                OpenIdConnectConstants.Scopes.Email,
                OpenIdConnectConstants.Scopes.Profile,
                OpenIdConnectConstants.Scopes.OfflineAccess,
                OpenIddictConstants.Scopes.Roles
            }.Intersect(request.GetScopes()));
 
            return ticket;
        }
    }
}

And the last, also in the /Api folder should be called ResourceController.cs and be edited to contain:

using A2SPA.Models;
using AspNet.Security.OAuth.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
 
namespace A2SPA.Api
{
    [Route("api")]
    public class ResourceController : Controller
    {
        private readonly UserManager<ApplicationUser> _userManager;
 
        public ResourceController(UserManager<ApplicationUser> userManager)
        {
            _userManager = userManager;
        }
 
        [Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)]
        [HttpGet("message")]
        public async Task<IActionResult> GetMessage()
        {
            var user = await _userManager.GetUserAsync(User);
            if (user == null)
            {
                return BadRequest();
            }
 
            return Content($"{user.UserName} has been successfully authenticated.");
        }
    }
}

Next we'll turn our attention to the front end changes needed to support this.

Angular 2 changes to support authentication

In the  /wwwroot/app/models folder we'll create two new typescript files to mirror the new login view model and register view model.

First create LoginViewModel.ts in /wwwroot/app/models and edit it to this:

import { Component } from '@angular/core';
 
export class LoginViewModel {
    email: string;
    password: string;
}

Next create RegisterViewModel.ts in /wwwroot/app/models and edit it to this:

import { Component } from '@angular/core';
 
export class RegisterViewModel {
    email: string;
    password: string;
    verifyPassword: string;
}

Next create a new folder alongside ./models called ./security, that is /wwwroot/app/security and add into this a new typescript file called auth.service.ts which will contain a number of useful methods to build request headers we can add to our requests, as well as save our token into sessionstorage, when we have a successful login.

Edit this new file /app/security/auth.service.ts to read:

import { Component } from '@angular/core';
import { Injectable } from '@angular/core';
import { Headers } from '@angular/http';
import { OpenIdDictToken } from './OpenIdDictToken'
 
@Injectable()
export class AuthService {
 
    constructor() { }
 
    // for requesting secure data using json
    authJsonHeaders() {
        let header = new Headers();
        header.append('Content-Type', 'application/json');
        header.append('Accept', 'application/json');
        header.append('Authorization', 'Bearer ' + sessionStorage.getItem('bearer_token'));
        return header;
    }
 
    // for requesting secure data from a form post
    authFormHeaders() {
        let header = new Headers();
        header.append('Content-Type', 'application/x-www-form-urlencoded');
        header.append('Accept', 'application/json');
        header.append('Authorization', 'Bearer ' + sessionStorage.getItem('bearer_token'));
        return header;
    }
 
    // for requesting unsecured data using json
    jsonHeaders() {
        let header = new Headers();
        header.append('Content-Type', 'application/json');
        header.append('Accept', 'application/json');
        return header;
    }
 
    // for requesting unsecured data using form post
    contentHeaders() {
        let header = new Headers();
        header.append('Content-Type', 'application/x-www-form-urlencoded');
        header.append('Accept', 'application/json');
        return header;
    }
 
    // After a successful login, save token data into session storage
    // note: use "localStorage" for persistent, browser-wide logins; "sessionStorage" for per-session storage.
    login(responseData: OpenIdDictToken) {
        let access_token: string = responseData.access_token;
        let expires_in: number = responseData.expires_in;
        sessionStorage.setItem('access_token', access_token);
        sessionStorage.setItem('bearer_token', access_token);
        // TODO: implement meaningful refresh, handle expiry 
        sessionStorage.setItem('expires_in', expires_in.toString());
    }
 
    // called when logging out user; clears tokens from browser
    logout() {
        //localStorage.removeItem('access_token');
        sessionStorage.removeItem('access_token');
        sessionStorage.removeItem('bearer_token');
        sessionStorage.removeItem('expires_in');
    }
 
    // simple check of logged in status: if there is a token, we're (probably) logged in.
    // ideally we check status and check token has not expired (server will back us up, if this not done, but it could be cleaner)
    loggedIn() {
        return !!sessionStorage.getItem('bearer_token');
    }
}

 

The next new file should be called auth-guard.service.ts and be added into the same folder /wwwroot/app/security and should contain this:

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { CanActivate } from '@angular/router';
import { AuthService } from './auth.service';
 
@Injectable()
export class AuthGuard implements CanActivate {
 
    constructor(private authService: AuthService, private router: Router) { }
 
    canActivate() {
        if (!this.authService.loggedIn()) {
            this.router.navigate(['/login']);
            return false;
        }
        return true;
    }
}

Lastly we'll add one last typescript file, a data model, into the /wwwroot/app/security folder. This should be called OpenIdDictToken.ts and contain the following:

import { Component } from '@angular/core';
 
export class OpenIdDictToken {
    access_token: string;
    expires_in: number;
    refresh_token: string;
    token_type: string;
}

In the /wwwroot/app directory we'll next add a couple of new typescript components. First create register.component.ts which will handle new user registrations, it should contain the following:

import { Component, OnInit } from '@angular/core';
import { Title }     from '@angular/platform-browser';
import { Router } from '@angular/router';
 
import { Http } from '@angular/http';
import { AuthService } from './security/auth.service';
import { RegisterViewModel } from './models/RegisterViewModel';
 
@Component({
    selector: 'register',
    templateUrl: '/partial/registerComponent'
})
 
export class RegisterComponent {
    registerViewModel: RegisterViewModel;
 
    constructor(public router: Router, private titleService: Title, public http: Http, private authService: AuthService) { }
 
    ngOnInit() {
        this.registerViewModel = new RegisterViewModel();
    }
 
    setTitle(newTitle: string) {
        this.titleService.setTitle(newTitle);
    }
 
    register(event: Event): void {
        event.preventDefault();
        let body = { 'email': this.registerViewModel.email, 'password': this.registerViewModel.password, 'verifyPassword': this.registerViewModel.verifyPassword };
 
        this.http.post('/Account/Register', JSON.stringify(body), { headers: this.authService.jsonHeaders() })
            .subscribe(response => {
                if (response.status == 200) {
                    this.router.navigate(['/login']);
                } else {
                    alert(response.json().error);
                    console.log(response.json().error);
                }
            },
            error => {
                // TODO: parse error messages, generate toast popups
                // {"Email":["The Email field is required.","The Email field is not a valid e-mail address."],"Password":["The Password field is required.","The Password must be at least 6 characters long."]}
                alert(error.text());
                console.log(error.text());
            });
    }
}

Next add beside it another new typescript file login.component.ts which will handle our logins, once we have our new end user registered. Edit login.component.ts to read:

import { Component } from '@angular/core';
import { Title }     from '@angular/platform-browser';
import { Router } from '@angular/router';
 
import { Http } from '@angular/http';
import { AuthService } from './security/auth.service';
import { LoginViewModel } from './models/LoginViewModel';
 
@Component({
    selector: 'login',
    templateUrl: '/partial/loginComponent'
})
 
export class LoginComponent {
    loginViewModel: LoginViewModel;
 
    constructor(public router: Router, private titleService: Title, public http: Http, private authService: AuthService) { }
 
    ngOnInit() {
        this.loginViewModel = new LoginViewModel();
    }
 
    public setTitle(newTitle: string) {
        this.titleService.setTitle(newTitle);
    }
 
    // post the user's login details to server, if authenticated token is returned, then token is saved to session storage
    login(event: Event): void {
        event.preventDefault();
        let body = 'username=' + this.loginViewModel.email + '&password=' + this.loginViewModel.password + '&grant_type=password';
 
        this.http.post('/connect/token', body, { headers: this.authService.contentHeaders() })
            .subscribe(response => {
                // success, save the token to session storage
                this.authService.login(response.json());
                this.router.navigate(['/about']);
            },
            error => {
                // failed; TODO: add some nice toast / error handling
                alert(error.text());
                console.log(error.text());
            }
            );
    }
}

We'll next update the existing app.routing.ts file to include our new components and mark the about component for authenticated access only. Update app.routing.ts to this:

import { Routes, RouterModule } from '@angular/router';
import { AuthGuard } from './security/auth-guard.service';
 
import { AboutComponent } from './about.component';
import { IndexComponent } from './index.component';
import { ContactComponent } from './contact.component';
import { LoginComponent } from './login.component';
import { RegisterComponent } from './register.component';
 
const appRoutes: Routes = [
    { path: '', redirectTo: 'home', pathMatch: 'full' },
    { path: 'home', component: IndexComponent, data: { title: 'Home' } },
    { path: 'login', component: LoginComponent, data: { title: 'Login' } },
    { path: 'register', component: RegisterComponent, data: { title: 'Register' } },
    { path: 'about', component: AboutComponent, data: { title: 'About' }, canActivate: [AuthGuard]  },
    { path: 'contact', component: ContactComponent, data: { title: 'Contact' }}
];
 
export const routing = RouterModule.forRoot(appRoutes);
 
export const routedComponents = [AboutComponent, IndexComponent, ContactComponent, LoginComponent, RegisterComponent];

 

The file app.module.ts also needs updating, to include references to our AuthService and AuthGuard services; update app.module.ts  to this:

import { NgModule, enableProdMode } from '@angular/core';
import { BrowserModule, Title } from '@angular/platform-browser';
import { routing, routedComponents } from './app.routing';
import { APP_BASE_HREF, Location } from '@angular/common';
import { AppComponent } from './app.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule  } from '@angular/http';
import { SampleDataService } from './services/sampleData.service';
import { AuthService } from './security/auth.service';
import { AuthGuard } from './security/auth-guard.service';
import './rxjs-operators';
 
// enableProdMode();
 
@NgModule({
    imports: [BrowserModule, FormsModule, HttpModule, routing],
    declarations: [AppComponent, routedComponents],
    providers: [SampleDataService,
        AuthService,
        AuthGuard, Title, { provide: APP_BASE_HREF, useValue: '/' }],
    bootstrap: [AppComponent]
})
export class AppModule { }

Since we're making our about component only accessible to logged on users, we'll alter the calls to use headers that include the JWT token.  Since these headers will see some re-use we've already put them into the auth.service.ts file we created earlier. In this case

import { Component, OnInit } from '@angular/core';
import { SampleDataService } from './services/sampleData.service';
import { TestData } from './models/testData';
 
@Component({
    selector: 'my-about',
    templateUrl: '/partial/aboutComponent'
})
 
export class AboutComponent implements OnInit {
    testData: TestData;
    errorMessage: string;
 
    constructor(private sampleDataService: SampleDataService) { }
 
    ngOnInit() {
        this.getTestData();
    }
 
    getTestData() {
        this.sampleDataService.getSampleData()
            .subscribe((data: TestData) => this.testData = data,
            error => this.errorMessage = <any>error);
    }
 
    addTestData(event: Event):void {
        event.preventDefault();
        if (!this.testData) { return; }
        this.sampleDataService.addSampleData(this.testData)
            .subscribe((data: TestData) => this.testData = data,
            error => this.errorMessage = <any>error);
    }
}

Our app.component.ts file  will support the logout call as well as provide a wrapper to the isLoggedIn service; this will be used to hide and unhide register and login (when not logged in) or logout (when logged in), as well as hide various menu options if not available to users who are not logged in (such as about component, in the app.component view earlier).

Edit the app.component.ts file to this:

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router';
 
import { Http } from '@angular/http';
import { AuthService } from './security/auth.service';
 
@Component({
    selector: 'my-app',
    templateUrl: '/partial/appComponent'
})
export class AppComponent {
    angularClientSideData = 'Angular';
 
    public constructor(private router: Router, private titleService: Title, private http: Http, private authService: AuthService) { }
 
    // wrapper to the Angular title service.
    public setTitle(newTitle: string) {
        this.titleService.setTitle(newTitle);
    }
 
    // provide local page the user's logged in status (do we have a token or not)
    public isLoggedIn(): boolean {
        return this.authService.loggedIn();
    }
 
    // tell the server that the user wants to logout; clears token from server, then calls auth.service to clear token locally in browser
    public logout() {
        this.http.get('/connect/logout', { headers: this.authService.authJsonHeaders() })
            .subscribe(response => {
                // clear token in browser
                this.authService.logout();
                // return to 'home' page
                this.router.navigate(['']);
            },
            error => {
                // failed; TODO: add some nice toast / error handling
                alert(error.text());
                console.log(error.text());
            }
            );
    }
}

Since we've now a common set of four different headers (for each of form posts vs JSON calls, and logged on vs not logged on), we'll refactor the SampleData.service.ts file to use one of these new headers, and clean up plus add a few comments. Change SampleData.service.ts to this:

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import { Observable }     from 'rxjs/Observable';
import { TestData } from '../models/testData';
import { AuthService } from '../security/auth.service';
 
@Injectable()
export class SampleDataService {
 
    private url: string = 'api/sampleData';
 
    constructor(private http: Http, private authService: AuthService) { }
 
    getSampleData(): Observable<TestData> {
        return this.http.get(this.url, { headers: this.authService.authJsonHeaders() })
            .map((resp: Response) => resp.json())
            .catch(this.handleError);
    }
 
    addSampleData(testData: TestData): Observable<TestData> {
        return this.http
            .post(this.url, JSON.stringify(testData), { headers: this.authService.authJsonHeaders() })
            .map((resp: Response) => resp.json())
            .catch(this.handleError);
    }
 
    // from <a href="https://angular.io/docs/ts/latest/guide/server-communication.html">https://angular.io/docs/ts/latest/guide/server-communication.html</a>
    private handleError(error: Response | any) {
        // In a real world app, we might use a remote logging infrastructure
        let errMsg: string;
        if (error instanceof Response) {
            const body = error.json() || '';
            const err = body.error || JSON.stringify(body);
            errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
        } else {
            errMsg = error.message ? error.message : error.toString();
        }
        console.error(errMsg);
        return Observable.throw(errMsg);
    }
}

Finally edit the /wwwroot/css/site.css to add this new style to support the hidden attribute:

/* hidden .. see: <a href="http://www.talkingdotnet.com/dont-use-hidden-attribute-angularjs-2/">http://www.talkingdotnet.com/dont-use-hidden-attribute-angularjs-2/</a> */
[hidden] {
  display: none !important;
}

To prevent our username and password boxes from triggering the form error, we'll alter our tag helper to wrap the validation DIV that is there with another

 

Edit the /helpers/TagDiTagHelper.cs file to include these two lines:

// set up validation conditional DIV's here; only show error if modifications to form have been made
TagBuilder outerValidationBlock = new TagBuilder("div");
outerValidationBlock.MergeAttribute("*ngIf", string.Format("({0}.dirty || {0}.touched)", propertyName));

Immediately above the existing validation DIV block these two lines of code:

// .. and then, only if an error in data entry
TagBuilder validationBlock = new TagBuilder("div");
validationBlock.MergeAttribute("*ngIf", string.Format("{0}.errors", propertyName));
validationBlock.MergeAttribute("class", "alert alert-danger");

And change the last line of code that used to read this:

            output.Content.AppendHtml(validationBlock);
        }
    }
}

to include the existing validation block inside our new outer validation DIV block, so that it reads this:

            // add the validation prepared earlier, to the end, last of all
            outerValidationBlock.InnerHtml.AppendHtml(validationBlock);
            output.Content.AppendHtml(outerValidationBlock);
        }
    }
}

All done, ready to test. As I have not added a range of database update methods, again to keep the code simple and focussed, manually delete the existing database, using SQL manager, to allow the  database initializer code in startup.cs to work.

Build the code, press ctrl-F5. The browser will launch, and as this is the first time the code is executed (this time around) and as long as the code is in debug mode, it will create the database afresh, but this time with the new authentication tables as well as our testdata table, and should still be including the 1 line of seed data as before.

If all is well you should also see the home page.

Try to manually go to the about page /about, you should get taken to the login page.

As we've not yet registered (you could see a pre-registered access if you wish), click on the register link, fill in a username, enter a password twice, then click Submit:

Once registered you'll be taken to the login page.

Now on the login page you need to enter the just invented username and password, whereupon you'll get taken to the about page. This is still the same about page (more or less) as in part 3, except it is now secured using OpenIdDIct and tokens.

Look at the menus, you'll see the logged in view in place, where we have no longer got Register or Login menu links, but instead a Logout menu link. Click Logout and the about page is once again locked, Logout links is hidden, taken from the menu, and the Register or Login menu links are unhidden.

If you have Chrome, try adding the Augury plugin. This is designed for Angular 2 and can give you an insight into what is happening in your application

 

Behind the scenes you can watch the network traffic, using F12 to view this, then select the network tab. Here we have Firefox showing the token being returned from a successful login:

Chrome and Firefox (depending on your browser settings) let you easily see the SessionStorage too:

When you are logged in, it will include a token, which is removed from the browser (and invalidated a the server) when you log off. For instance here is the token being sent in a request header from the client, to the server, as we get some data. In Internet Explorer we can see the token:

Where to now?

In the next part, part 5, we'll update our Web API data services, add a client side datagrid, then convert our Web API services to async.

Next we'll demonstrate using NSwag to generate Typescript data models and data services for Angular 2 directly from your C# data models and Web API classes/methods.

NSwag can also be used if you need to publish or document your Web API methods, just as you might with Swagger.

This will make changes simpler than before; any time there's a new property added you can easily add support - all done simply and type safe, and then leave you to just your Angular 2 component and template to suit.

Points of Interest

Again, as the focus has been on integration between ASP.Net Core and Angular 2, this series has been focussing more on that interaction, rather than cover every single facet in great detail.

There is still work to be done in tag helpers (refactoring and covering other data types), as well as cleaning up our services, including the addition of async. A few enhancements you might consider from this part, part 4, around authentication include:

User roles - you have available the already complete but extensible authentication and roles objects. You can customise these if you wish, but will likely find they do most of what you need, out of the box.

User management -  You have tag helpers to save time, create yourself some services, components and views to allow you to edit your user details, add new users, or let your users update their details.

Error messages - Use one of the many 'toast' utilities out there to add cleaner error and success messages.

Handling timeouts and allowing refresh or session extension - You can set a timer or alert running so that as a user nears the end of their session, they have a warning that time is almost up. You might like to provide them a refresh or extension option, or simply say - time's up. Either way the time is available to you in the return data sent with the token as you logon.

History

Part 1 - How to integrate ASP.Net Core and Angular 2

Part 2 - How to use Tag Helpers to display data

Part 3 - How to use Tag Helpers for data input, added SQL backend using EF Core. 

Part 4 - How to add Token Authentication using JWT and OpenIdDict (this article)

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Robert_Dyball
Software Developer (Senior)
Australia Australia
No Biography provided

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web06 | 2.8.190214.1 | Last Updated 9 Mar 2017
Article Copyright 2017 by Robert_Dyball
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid