This part of the series will cover the topic of security. You'll also see an example of how our simple tag helpers make data entry and validation easier.
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.
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:
options.UseOpenIddict();
});
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<A2spaContext>()
.AddDefaultTokenProviders();
services.AddOpenIddict()
to this:
options.UseOpenIddict();
});
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<A2spaContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
});
services.AddOpenIddict()
The 3rd line of claims added was remarked out, as 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 using
s:
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)
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 has passed this validation phase, 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 helps ensure that the token has not been tampered 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
https://github.com/openiddict/openiddict-samples/tree/8032ef4b63c97bb26af0785ed1b317e7cc2b1247/samples
Updated 22nd April 2021 Note: the above link is from an earlier 30th Sep 2018 commit.
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.
="1.0"="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; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<A2spaContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
options.UseOpenIddict();
});
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<A2spaContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
});
services.AddOpenIddict()
.AddEntityFrameworkCoreStores<A2spaContext>()
.AddMvcBinders()
.EnableTokenEndpoint("/connect/token")
.AllowPasswordFlow()
.DisableHttpsRequirement();
}
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");
}
app.UseOAuthValidation();
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?}");
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.cshtml 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 are 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
{
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;
}
[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);
}
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:
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."
});
}
if (!await _signInManager.CanSignInAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
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."
});
}
if (_userManager.SupportsUserLockout &&
await _userManager.IsLockedOutAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
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);
}
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)
{
var principal = await _signInManager.CreateUserPrincipalAsync(user);
foreach (var claim in principal.Claims)
{
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
var ticket = new AuthenticationTicket(
principal, new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
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() { }
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;
}
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;
}
jsonHeaders() {
let header = new Headers();
header.append('Content-Type', 'application/json');
header.append('Accept', 'application/json');
return header;
}
contentHeaders() {
let header = new Headers();
header.append('Content-Type', 'application/x-www-form-urlencoded');
header.append('Accept', 'application/json');
return header;
}
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);
sessionStorage.setItem('expires_in', expires_in.toString());
}
logout() {
sessionStorage.removeItem('access_token');
sessionStorage.removeItem('bearer_token');
sessionStorage.removeItem('expires_in');
}
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 => {
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);
}
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 => {
this.authService.login(response.json());
this.router.navigate(['/about']);
},
error => {
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';
@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) { }
public setTitle(newTitle: string) {
this.titleService.setTitle(newTitle);
}
public isLoggedIn(): boolean {
return this.authService.loggedIn();
}
public logout() {
this.http.get('/connect/logout', { headers: this.authService.authJsonHeaders() })
.subscribe(response => {
this.authService.logout();
this.router.navigate(['']);
},
error => {
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);
}
private handleError(error: Response | any) {
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] {
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:
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:
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:
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 focused, 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, 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 focusing 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 customize 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