In my last article, Security in Angular: Part 1, you learned to create a set of Angular classes to support user authentication and authorization. You used these classes to log in a user and make menus and buttons visible and invisible. In this article, you'll use a .NET Core Web API project to authenticate a user against a SQL Server table. You'll return an authorization
object by reading claim information from a claim table. You'll also secure your Web API methods using the JSON Web Token (JWT) standard.
Related articles
There are several steps you need to perform to secure your Web API methods. You'll be guided through each of the following steps in this article:
- Download and set up a sample application and database.
- Create Entity Framework security classes.
- Create a class to authenticate users and return an authorization object.
- Create a security controller.
- Modify the Angular application to call a security controller.
- Add JSON Web Tokens to secure Web API calls.
- Create an HTTP interceptor.
Download the Starting Application
In the last article, all the data for users, products, and categories were in mock
classes. For this article, I've provided you with a starting application that contains an ASP.NET Core Web API project to retrieve product and category data from Web API method calls. In this article, you build the Web API security
classes to authenticate a user and return an authorization
object to your Angular application.
Download the starting sample for this article from the CODE Magazine website, or from http://pdsa.com/downloads. Select “PDSA/Fairway Articles” from the Category drop-down, and then choose Security in Angular - Part 2. After you've downloaded the ZIP file, there are two folders within the ZIP entitled Start and End. Extract all of the files and folders within the Start folder to a folder somewhere on your hard drive. You can then follow along with this article to build all of the Web APIs to secure your Angular application.
Load the Visual Studio Code Workspace
After extracting the Start folder from the ZIP file, double-click on the PTCApp.code-workspace
file to load the two projects in this application. If you double-click on this workspace file, the solution is loaded that looks like Figure 1. There are two projects: PTC
is the Angular application and PtcApi
is the ASP.NET Core Web API project.
The PTC Database
There's a SQL Server Express database named PTC
included in the ZIP file. Open the PtcDbContext.cs
file located in the \PtcApi\Model
folder. Change the path in the connection string constant to point to the folder in which you installed the files from this ZIP file. If you don't have SQL Server Express installed, you can use the PTC.sql
file located in the \SqlData
folder to create the appropriate tables in your own SQL Server instance.
Security Tables Overview
The PTC database has two tables in addition to the product and category tables: User and UserClaim (Figure 2). These tables are like the ones you find in the ASP.NET Identity System from Microsoft. I've simplified the structure just to keep the code small for this sample application.
User Table
The user table generally contains information about a specific user, such as their user name, password, first name, last name, etc. For the purpose of this article, I've simplified this table to only a unique ID (UserId), the name for the log in (UserName), and the Password for the user, as shown in Figure 3. Please note that I'm using a plain-text password in the sample for this application. In a production application, this password would be either encrypted or hashed.
User Claim Table
In the UserClaim table, there are four fields: ClaimId, UserId, ClaimType, and ClaimValue, (as shown in Figure 4). The ClaimId is a unique identifier for the claim record. The UserId is a foreign key relation to the User table. The value in the ClaimType field needs to match the exact property name you create in the AppUserAuth
class from Angular, and the one you're going to create in C# in the next section of this article. The value in the ClaimValue should be a true
value for any ClaimType you enter for that user.
Don't enter a record for a specific user and claim type if you don't wish to give the user that claim. For example, the CanAddProduct
property in the authorization
object should be set to false
for the user BJones. Therefore, don't enter a record in the UserClaim table for BJones with a ClaimType equal to CanAddProduct. Later in this article, you'll learn how this process works.
Create C# Security Classes
In the Security in Angular - Part 1 article, you created Angular classes to represent a user and a user authorization
object with properties to bind to. Create these same classes using C# within the PtcApi
project. You're going to build the C# classes with the same class and property names as those you created in TypeScript.
AppUser Class
The AppUser
class is an Entity Framework class that represents a single record you retrieve from the User table. For each property in the User table, create a corresponding property with the appropriate data annotations. Right mouse-click on the Model
folder and add a new file named AppUser.cs
. Add the code shown in Listing 1 into this new file.
Listing 1: Create a AppUser Entity Framework class
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PtcApi.Model
{
[Table("User", Schema = "Security")]
public partial class AppUser
{
[Required()]
[Key()]
public Guid UserId { get; set; }
[Required()]
[StringLength(255)]
public string UserName { get; set; }
[Required()]
[StringLength(255)]
public string Password { get; set; }
}
}
The Using statements in this file are used to supply the appropriate data annotations for the Entity Framework. The [Table]
attribute specifies the name of the table this class maps to: User
. The User table is located in the database schema named Security, so pass the Schema
property to this attribute as well.
The [Required]
attribute should be added to all fields, because all fields in the User table are marked as not allowing null values. The [StringLength]
attribute can be added to the UserName and Password fields in case you want to perform some validation on the data prior to inserting or updating. This article won't cover this, but I like adding appropriate data annotations to my properties.
Create AppUserClaim Class
The AppUserClaim
class is an Entity Framework class that represents a single record you retrieve from the UserClaim table. For each property in the UserClaim table, create a corresponding property with the appropriate data annotations. Right mouse-click on the Model
folder and add a new file named AppUserClaim.cs
. Add the code shown in Listing 2.
Listing 2: Create a AppUserClaim Entity Framework class
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PtcApi.Model
{
[Table("UserClaim", Schema = "Security")]
public partial class AppUserClaim
{
[Required()]
[Key()]
public Guid ClaimId { get; set; }
[Required()]
public Guid UserId { get; set; }
[Required()]
[StringLength(100)]
public string ClaimType { get; set; }
[Required()]
[StringLength(50)]
public string ClaimValue { get; set; }
}
}
The Using statements and attributes used are similar to what you used in the AppUser
class. The [Table]
attribute specifies the UserClaim table, located in the Security
schema. All of the other attributes attached to the properties are self-explanatory.
Modify PtcDbContext Class
When using the Entity Framework, you typically create a class that inherits from the Entity Framework's DbContext
class. In the PtcApi
project, this class is named PtcDbContext
. For each of the EF classes you create, add a collection property of the type DbSet<T>
to this DbContext
class. Open the PtcDbContext.cs
file in the Model
folder and add two new properties for the AppUser
and AppUserClaim
classes, as shown in the code snippet below.
public DbSet<AppUser> Users { get; set; }
public DbSet<AppUserClaim> Claims { get; set; }
Once you have these two properties created, you can retrieve users and claims from them. You may also use these properties to add, edit, and delete records from each table.
Create Authorization Class
If you remember from Part 1 of this article series, you created a user authorization
class in TypeScript that contained properties to make menus and buttons visible and invisible. You need to create this same class in C# so it may be returned from a Web API method call and mapped to the TypeScript class in Angular. Right mouse-click on the Model
folder and add a new file named AppUserAuth.cs
. Add the code shown in Listing 3 into this file.
Listing 3: Create a user authorization object with properties to match those in TypeScript
namespace PtcApi.Model
{
public class AppUserAuth
{
public AppUserAuth()
{
UserName = "Not authorized";
BearerToken = string.Empty;
}
public string UserName { get; set; }
public string BearerToken { get; set; }
public bool IsAuthenticated { get; set; }
public bool CanAccessProducts { get; set; }
public bool CanAddProduct { get; set; }
public bool CanSaveProduct { get; set; }
public bool CanAccessCategories { get; set; }
public bool CanAddCategory { get; set; }
}
}
Create Security Manager Class
There are several steps that must be accomplished to authenticate a user and create an authorization
object to send back to Angular. Instead of writing all of that code within a controller, build a class named SecurityManager
for that logic. The controller may then create an instance of that class and call one method to authenticate the user and to retrieve an authorization
object. Right mouse-click on the Model
folder and add a new file named SecurityManager.cs
. Add the code shown in Listing 4 into this file.
Listing 4: Build a security manager class instead of writing a lot of code in a controller class.
using System;
using System.Collections.Generic;
using System.Linq;
using PtcApi.Model;
namespace PtcApi.Security
{
public class SecurityManager
{
public AppUserAuth
AuthenticateUser(AppUser user)
{
AppUserAuth ret = new AppUserAuth();
AppUser authUser = null;
using (var db = new PtcDbContext())
{
// Attempt to validate user
authUser = db.Users.Where(
u => u.UserName.ToLower()
== user.UserName.ToLower()
&& u.Password
== user.Password).FirstOrDefault();
}
if (authUser != null)
{
// Build User Security Object
ret = BuildUserAuthObject(authUser);
}
return ret;
}
protected List<AppUserClaim> GetUserClaims(AppUser authUser)
{
List<AppUserClaim> list = new List<AppUserClaim>();
using (var db = new PtcDbContext())
{
list = db.Claims.Where(
u => u.UserId == authUser.UserId)
.ToList();
}
return list;
}
protected AppUserAuth
BuildUserAuthObject(AppUser authUser)
{
AppUserAuth ret = new AppUserAuth();
List<AppUserClaim> claims = new List<AppUserClaim>();
// Set User Properties
ret.UserName = authUser.UserName;
ret.IsAuthenticated = true;
ret.BearerToken = new Guid().ToString();
// Get all claims for this user
claims = GetUserClaims(authUser);
// Loop through all claims and set properties of user object
foreach (AppUserClaim claim in claims)
{
try
{
typeof(AppUserAuth)
.GetProperty(claim.ClaimType)
.SetValue(ret, Convert.ToBoolean(claim.ClaimValue), null);
}
catch
{
}
}
return ret;
}
}
}
The AuthenticateUser() Method
The AuthenticateUser()
method is the only method exposed from the SecurityManager
class. The security controller you're going to create in the next section of this article accepts an AppUser
object from the Angular application via a Web API call. You're going to pass this AppUser
object to the AuthenticateUser()
method.
The AuthenticateUser()
method creates an instance of a PtcDbContext
class. Once this object is created, the Users
property is accessed and a where filter is applied to locate a user in the User table where the UserName
field matches the UserName
property passed in from the Angular application. In addition, the password must match the value in the User table as well.
If a valid user is located in the User table, the AppUser
object retrieved from the Entity Framework is passed on to the BuildUserAuthObject()
method. It's in this method that the user authorization
object is created.
The BuildUserAuth() Method
This method is responsible for building the user authorization
object that's sent back to Angular with all the appropriate Boolean properties set. The first thing this method does is create a new AppUserAuth
object. The UserName
property is set with the authenticated user name passed in. The IsAuthenticated
property is set to true
to signify that the user is valid. The BearerToken
property is going to be set to a random GUID for now. Later in this article, you'll fill this in with a real bearer token generated using the JSON Web Token system.
Next, all the claims in the UserClaim table for this user are gathered by calling a method named GetUserClaims()
. Once you have that list of claims, you loop through each claim one-by-one. Each time through the loop, use reflection on the AppUserAuth
object to attempt to access the property name that's in the ClaimType
property read in from the UserClaim table. This is why I mentioned earlier that the names you add to the UserClaim table must match the properties in the AppUserAuth
class, as it allows you to use .NET reflection to set each property value to the value contained in the ClaimValue
field.
Once this method has looped through all claims for the authenticated user, the AppUserAuth
properties have either been set to a true
value, or, if the property didn't exist in the UserClaim table, then the default value of false
is in the other properties.
NOTE: This method of setting properties is fine when you don't have a lot of security in your Angular application. For an application with many security claims, you should return the list of claims as part of the user authorization
object. Of course, once you have a list of claims, and not individual properties, you can't take advantage of binding properties to a menu or a button. Instead, you'll have to create a custom structural directive in Angular to make menus and buttons visible and invisible. Creating this type of security will be the topic of discussion in the third part of this series on Angular security.
The GetUserClaims() Method
The GetUserClaims()
method is where you retrieve a list of claims for the user who's been authenticated. In this method, you create a new instance of the PtcDbContext
class. Access the Claims
property and apply a Where
filter to only retrieve those claims that match the UserId
property from the authenticated user to those records in the UserClaim table that have that same UserId
value. The list of claims is returned from this method and used in the BuildUserAuth()
method to set the appropriate properties in the user authorization
object.
Add Security Controller
It's now time to build the security controller that the Angular application calls. Angular passes the user credentials in an AppUser
object to the Web API method. The method returns an AppUserAuth
object with the appropriate properties set by calling the AuthenticateUser()
method. Right mouse-click on the Controllers
folder and add a new file named SecurityController.cs
. Add the code shown in Listing 5 into this file.
Listing 5: The Security Controller authenticates the user and returns the authorization object
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PtcApi.Security;
using PtcApi.Model;
namespace PtcApi.Controllers
{
[Route("api/[controller]")]
public class SecurityController : BaseApiController
{
[HttpPost("login")]
public IActionResult Login([FromBody]AppUser user)
{
IActionResult ret = null;
AppUserAuth auth = new AppUserAuth();
SecurityManager mgr = new SecurityManager();
auth = mgr.AuthenticateUser(user);
if (auth.IsAuthenticated)
{
ret = StatusCode(StatusCodes.Status200OK, auth);
}
else
{
ret = StatusCode(StatusCodes.Status404NotFound, "Invalid User Name/Password.");
}
return ret;
}
}
}
The Login() Method
The Login()
method accepts an AppUser
object from the Angular application. Next, it creates an instance of the SecurityManager
and passes the AppUser
object to the AuthenticateUser()
method. The AppUserAuth
object returned from the AuthenticateUser()
method has the IsAuthenticated
property set to a true
value if the user has been validated in the User table, and the security properties have been set.
If the user is authenticated, call the StatusCode()
method passing in an enumeration to specify a status code of 200 (OK), and passing in the user authorization
object. This method generates an IActionResult
object to tell Angular that the call to the Login()
method succeeded and that the resulting data is in the payload passed back.
If the user isn't authenticated, pass in an enumeration of 404 (NotFound) to the StatusCode()
method, and a string specifying that the user name and password combination wasn't valid. When this code is returned to Angular, it signifies an error condition, and your Angular code needs to handle this, as appropriate.
Call the Web API from Angular
It's now time to replace the mock calls for authentication and authorization in Angular with calls to the new Web API Login()
method. You need to add the HttpClient
, HttpHeaders
, and the tap
classes to your SecurityService
class. Go back to the PTC
project and open the security.service.ts
file located in the \src\app\security
folder and add the following import statements.
import {HttpClient, HttpHeaders}
from '@angular/common/http';
import { tap } from 'rxjs/operators/tap';
Add two constants just under the import statements. The first constant is the location of the Web API controller. The second constant holds header options required by the HttpClient
when POSTing data to a Web API call. You're going to use an HTTP POST to send the AppUser
object containing the user name and password to the Login()
method.
const API_URL = "http://localhost:5000/api/security/";
const httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'})
};
Have Angular inject the HttpClient
service into your security service class so you can make the HTTP call to the Login()
method. Dependency injection is used extensively throughout Angular to provide services such as HttpClient
to different components. The HttpClientModule
has already been imported in the AppModule
class. This module must always be imported to allow components to use the HttpClient
service.
constructor(private http: HttpClient) { }
Modify the Login() Method
Within the security service class, you need to remove the hard-coded logic in the login()
method. Modify the login()
method as shown in Listing 6 to call the Login()
method in the SecurityController
class. If valid data is returned from this call, the pipe()
method is used to pass that data to the tap()
method. Within the tap()
method, assign the data returned to the securityObject
property.
Listing 6: Modify the login() method to make the Web API call.
login(entity: AppUser): Observable<AppUserAuth> {
// Initialize security object
this.resetSecurityObject();
return this.http.post<AppUserAuth>(
API_URL + "login",
entity, httpOptions).pipe(
tap(resp => {
// Use object assign to update the current object
Object.assign(this.securityObject, resp);
// Store into local storage
localStorage.setItem("bearerToken",
this.securityObject.bearerToken);
}));
}
It's very important that you don't use the equal sign to assign to the securityObject
property. If you do, bound references to the securityObject
are wiped out, and your menus and other UI elements bound to properties on the securityObject
no longer work. Instead, use the Object.assign()
method to copy the data from one object to another.
It's very important that you don't use the equal sign to assign to the
securityObject
property.
Within the tap()
method, store the bearer token into local storage. The reason for storing this token is used when building an HTTP Interceptor
class. You'll learn to create this class a little later in this article.
Modify the Login Component
Because it's possible that the Web API may return a status code of 404 (not found), be sure to handle this code so you can display the Invalid User Name/Password error message to your user. Open the login.component.ts
file and add the error block in the subscribe()
method. The login HTML contains a Bootstrap alert that has the words Invalid User Name/Password. in a <p>
tag. This alert only shows up if the securityObject
is not null
and the isAuthenticated
property is set to false
. In the error block, assign a new instance of the AppUserAuth
class to the securityObject
property.
login() {
this.securityService.login(this.user)
.subscribe(resp => {
this.securityObject = resp;
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
}
}, () => {
// Display error message
this.securityObject = new AppUserAuth();
});
}
Try It Out
Save all of your changes and make sure that the Web API project and the Angular project is running. Go to the browser and attempt to log in using either “psheriff” or “bjones” with a password of "P@ssw0rd. If you've done everything correctly, the Web API is now being called for authenticating the user name and password. Try logging in with an invalid login ID and password to make sure the error handling is working.
Remove Mock Login File
Now that you're retrieving data from a Web API call and not the mock data, you can remove \security\login-mocks.ts
file. Once this file is removed, open the \security\security.service.ts
file and remove the import statement for the login-mocks
file.
Authorizing Access to the Web API Call
Now that you've created these Web API calls, you need to ensure that only those applications authorized to call your APIs can do so. In .NET, use the [Authorize]
attribute to secure a controller class, or individual methods within a controller class. However, there must be some authentication/authorization component in the .NET runtime to provide data to this attribute. The [Authorize]
attribute must be able to read that data to decide if the user has permissions to call the method.
There are many different authentication/authorization components you can use, such as Microsoft Identity, OAuth, and JSON Web Token (JWT), just to mention a few. In this article, you're going to use JSON Web Token.
Secure the Product Get() Method
To show you what happens when you apply the [Authorize]
attribute to a method, open the ProductController.cs
file and add the [Authorize]
attribute to the Get()
method, as shown in the code snippet below. You need to add a Using statement to use the [Authorize]
attribute. The Using statement is using Microsoft.AspNetCore.Authorization
.
[HttpGet]
[Authorize]
public IActionResult Get()
{
// REST OF THE CODE
}
Try It Out
If you haven't already done so, stop the PtcApi
Web API project from running. Save your changes and run the PtcApi
project again. Log in as “psheriff” and click on the Products menu. No product data is displayed because you attempted to call a method that was secured with the [Authorize]
attribute. Press the F12 key to bring up the developer tools and you should see something that looks like Figure 5.
Notice that you're getting a status code of 500 instead of a 401 (Unauthorized) or 403 (Forbidden). The reason is that you haven't registered an authentication service with .NET Core so the [Authorize]
attribute doesn't get any data and throws a generic exception. You must register and configure an authentication system such as Microsoft Identity, OAuth, or JWT to return a 401 instead of a 500.
Add the JWT Configuration
JSON Web Token (JWT) is a standard method to securely transmit data between your client and your server as a JSON object. The data contained within the object is digitally signed, which means that it can be verified and trusted. You can read more about JWT at http://www.jwt.io. To use the JSON Web Token system in .NET Core and Angular, there are a few steps you must perform.
- Add the JWT package to .NET Core.
- Add JWT bearer token checking package to .NET Core.
- Store default JWT settings in a configuration file.
- Read JWT settings from the configuration file and place into a singleton class.
- Register JWT as the authentication service.
- Add bearer token options to validate incoming tokens.
- Build a JWT token and add it to the user security object.
- Pass the JWT token back to Angular.
Add JWT Package
The first thing you must do in your .NET Core Web API project is to add some packages to use the JSON Web Token system. Open a terminal window in your PtcApi
project and enter the following command all on one line.
dotnet add package System.IdentityModel.Tokens.Jwt
After running this command, you'll be prompted to execute the restore command. Click on the Restore
button to execute this command.
Add Bearer Token Check
In addition to the JWT package, add a package to ensure the bearer token is passed in from the client. Add this package to your PtcApi
project using the following command in your terminal window.
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
After running this command, you'll be prompted to execute the restore command. Click on the Restore
button to execute this command.
Store JWT Information in appsettings.json
For the bearer token to be verifiable and trusted, you're going to need to generate a JSON Web Token object. There are four key pieces of information needed to create this object.
- A secret key used for hashing data sent to the client
- The name of the issuer of the token
- The intended audience of the token
- How many minutes to allow the token to be valid
You're going to need all of the listed items in two places in your code: once when you configure JWT in the .NET Core Startup
class, and once when you generate a new token specifically for a user. You don't want to hard-code data into two places, so use the .NET Core configuration system to retrieve this data from the appsettings.json
file located in the root folder of the PtcApi
project. Open the appsettings.json
file and add a new entry named JwtSettings
and add a new literal object, as shown in Listing 7.
Listing 7: Add your JWT information to the appsettings.json file.
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
},
"JwtSettings": {
"key": "This*Is&A!Long)Key(For%Creating@A$SymmetricKey",
"issuer": "http://localhost:5000",
"audience": "PTCUsers",
"minutesToExpiration": "10"
}
}
Add JwtSettings Class
You need a way to get the settings read in from the appsettings.json
file. An instance of a Configuration
object can be injected into any class to allow you to read settings from a JSON file. However, I prefer to use my own class with real properties for each property I create in the JSON file. Right mouse-click on the Model
folder and add a new file named JwtSettings.cs
and add the following code.
public class JwtSettings {
public string Key { get; set; }
public string Issuer { get; set; }
public string Audience { get; set; }
public int MinutesToExpiration { get; set; }
}
Read the Settings
Open the Startup.cs
file and add a new method named GetJwtSettings()
. In this method, create a new instance of the JwtSettings
class and use the Configuration
object, injected into the Startup
class, to set each property from the corresponding JSON property in the appsettings.json
file. After adding this method, you must add a using statement for the System
namespace.
public JwtSettings GetJwtSettings() {
JwtSettings settings = new JwtSettings();
settings.Key = Configuration["JwtSettings:key"];
settings.Audience = Configuration["JwtSettings:audience"];
settings.Issuer = Configuration["JwtSettings:issuer"];
settings.MinutesToExpiration = Convert.ToInt32(Configuration
["JwtSettings: minutesToExpiration"]);
return settings;
}
Create a Singleton for the JWT Settings
Locate the ConfigureServices()
method and create an instance of the JwtSettings
class near the top of the method. Call the GetJwtSettings()
method to read all of the settings into the JwtSettings
object. Add the instance of the JwtSettings
object as a singleton to the .NET core services so you can inject the JwtSettings
object into any controller.
public void ConfigureServices(IServiceCollection services)
{
// Get JWT Settings from JSON file
JwtSettings settings;
settings = GetJwtSettings();
services.AddSingleton<JwtSettings>(settings);
// REST OF THE CODE HERE
}
Register JWT as the Authentication Provider
Below the code you just added, add the code shown in Listing 8 to register JWT as an authentication provider. Call the AddAuthentication()
method on the services
object and set the two properties on the options, DefaultAuthenticateScheme
and DefaultChallengeScheme
, to the value JwtBearer
.
Listing 8: Add JWT as the authentication service to your Web API project
public void ConfigureServices(IServiceCollection services)
{
// Get JWT Token Settings from JSON file
JwtSettings settings;
settings = GetJwtSettings();
services.AddSingleton<JwtSettings>(settings);
// Register Jwt as the Authentication service
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "JwtBearer";
options.DefaultChallengeScheme = "JwtBearer";
})
.AddJwtBearer("JwtBearer", jwtBearerOptions =>
{
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(settings.Key)),
ValidateIssuer = true,
ValidIssuer = settings.Issuer,
ValidateAudience = true,
ValidAudience = settings.Audience,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(settings.MinutesToExpiration)
};
});
// REST OF THE CODE HERE
}
Next, call the AddJwtBearer()
method and set options for the bearer token using a TokenValidationParmeters
object. It's in this object that you set properties using the values read in from the appsettings.json
file. Most of these properties are self-explanatory. For additional information on what each one does, you can do a Google search on JWT and read one of any several articles on how to configure JWT.
After adding this code, you're going to have to add a couple more Using
statements. Namely; using Microsoft.IdentityModel.Tokens
and using System.Text
.
The last thing to do in the Startup
class is to modify the Configure()
method and tell it to use authentication, as shown in the following code snippet.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// REST OF THE CODE HERE
app.UseAuthentication();
app.UseMvc();
}
Inject Settings into SecurityController
Besides using the settings from the JSON file in the startup file, you also need them in the SecurityManager
class to build the bearer token. Because you create an instance of the SecurityManager
in the SecurityController
class, inject the JwtSettings
class into the SecurityController
class, as shown in Listing 9.
Listing 9: Inject settings and pass them to the SecurityManager object
public class SecurityController : Controller
{
private JwtSettings _settings;
public SecurityController(JwtSettings settings)
{
_settings = settings;
}
[HttpPost("login")]
public IActionResult
Login([FromBody]AppUser user)
{
IActionResult ret = null;
AppUserAuth auth = new AppUserAuth();
SecurityManager mgr = new SecurityManager(_settings);
auth = (AppUserAuth)mgr.AuthenticateUser(user);
}
// REST OF THE CODE HERE
}
Create a private field named _settings
in this class and set this field from the value injected in the constructor. Pass the _settings
value to the SecurityManager
constructor in the Login()
method. You haven't modified the constructor of the SecurityManager
to accept this yet, so VS Code will display an error, but you're going to add that code in the next section.
Accept Settings in SecurityManager
Open the SecurityManager.cs
file and add a private field and a constructor just like you did in the SecurityController
class. You're going to learn what you do with these settings in the next section of this article.
private JwtSettings _settings;
public SecurityManager(JwtSettings settings)
{
_settings = settings;
}
Build a JSON Web Token
You're finally ready to build a bearer token you can add to the BearerToken
property in the user security
object. Add a new method to the SecurityManager
class named BuildJwtToken()
. The first thing this method does is create a new SymmetricSecurityKey
object using the key you placed into the appsettings.json
file. Next, add two claims that are needed when generating a JSON Web Token; Subject (Sub
) and JSON Token ID (Jti
). Into the Sub
claim, place the user name, and into the Jti
claim, you just need a unique identifier, so generate a GUID.
Next, add the custom claims you need for your application. In this simple example, create one claim for each property in the AppUserAuth
class. Notice the use of the ToLower()
method to convert the True
and False
values to lower case. JavaScript and TypeScript always use lower case for the values true
and false
.
A new JwtSecurityToken
object is created and set with the properties from the JwtSettings
class. You must use the same values from the JwtSettings
class, just like you used in the Startup
class. If you don't, the token generated here won't match when it's passed back by Angular and is attempted to be verified by the code you wrote in the Startup
class.
The WriteToken()
method of the JwtSecurityTokenHandler
class is called to Base64 to encode the resulting string. It's this Base64 encoded string that's placed into the BearerToken
property in your user security
object that's passed to your Angular application. See Listing 10.
Listing 10: Build a JSON Web Token using the JSON settings and your claim data
protected string BuildJwtToken(AppUserAuth authUser)
{
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.Key));
List<Claim> jwtClaims = new List<Claim>();
// Create standard JWT claims
jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Sub, authUser.UserName));
jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));
// Add custom claims
jwtClaims.Add(new Claim("isAuthenticated", authUser.IsAuthenticated.ToString().ToLower()));
jwtClaims.Add(new Claim("canAccessProducts", authUser.CanAccessProducts.ToString().ToLower()));
jwtClaims.Add(new Claim("canAddProduct", authUser.CanAddProduct.ToString().ToLower()));
jwtClaims.Add(new Claim("canSaveProduct", authUser.CanSaveProduct.ToString().ToLower()));
jwtClaims.Add(new Claim("canAccessCategories", authUser.CanAccessCategories.ToString().ToLower()));
jwtClaims.Add(new Claim("canAddCategory", authUser.CanAddCategory.ToString().ToLower()));
// Create the JwtSecurityToken object
var token = new JwtSecurityToken(
issuer: _settings.Issuer,
audience: _settings.Audience,
claims: jwtClaims,
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddMinutes(_settings.MinutesToExpiration),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
// Create string representation of Jwt token
return new JwtSecurityTokenHandler().WriteToken(token);
}
After typing in all this code, you must add the following using statements at the top of the SecurityManager
class.
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
Call the BuildJwtToken() Method
Locate the BuildUserAuthObject()
method and add code to call this BuildJwtToken()
method and assign the string result to the BearerToken
property.
protected AppUserAuth
BuildUserAuthObject(AppUser authUser)
{
AppUserAuth ret = new AppUserAuth();
List<AppUserClaim> claims = new List<AppUserClaim>();
// Set User Properties
ret.UserName = authUser.UserName;
ret.IsAuthenticated = true;
ret.BearerToken = BuildJwtToken(ret);
// Rest of the code here
Try It Out
If you haven't already done so, stop the PtcApi
Web API project from running. Save your changes and run the PtcApi
project again. If you're logged into the Angular application, click the Logout menu. Enter either “psheriff” or “bjones” and a password of “P@ssw0rd” and you should see a screen that looks like Figure 6.
If you wish to see what makes up the bearerToken
property, you can decode the value at the www.jwt.io website. Copy the bearer token displayed on the log in page to the clipboard. Open a browser window and go to www.jwt.io. Scroll down on the page until you see a box labeled Encoded
. Delete what's in there and paste in your bearer token. You should immediately see the payload data with all your data, as shown in Figure 7.
Try It Out
Click on the Products menu and you should now see a generic 401 Unauthorized message in the developer tools console window that looks like Figure 8. The reason for this error is that the server doesn't know that you're the same person that just logged in. You must pass back the bearer token on each Web API call to prove to the server that you have permission to call the API.
Add Headers to Product Service
To avoid receiving the status code 401, pass the bearer token each time you make a Web API call from your Angular code. You're going to learn how to automatically add the token to every call in the next section of this article, but first, modify the Get()
method in the product service
class. Open the product.service.ts
file and modify the constructor to inject the SecurityService.
constructor(private http: HttpClient, private securityService: SecurityService) { }
Add code in the getProducts()
method to create a new HttpHeaders
object. Once you've instantiated this object, call the set()
method and pass in Authorization
as the header name. The data to pass in this header is the word Bearer
followed by a space, then the bearer token itself. Add a second parameter to the HttpClient's get()
method and pass an object with a property named headers
and the HttpHeaders
object you created as the value for that property.
getProducts(): Observable<Product[]> {
let httpOptions = new HttpHeaders()
.set('Authorization', 'Bearer ' + this.securityService.securityObject.bearerToken);
return this.http.get<Product[]> (API_URL, { headers: httpOptions });
}
Try It Out
Save your changes, go to the browser and login as “psheriff”, and click on the Products menu. You should see the product data displayed. This is because the server has authenticated your token and knows who you are. Thus, you're granted access to the Get()
method in the ProductController
.
HTTP Interceptor
You're going to have many Web API calls in a typical Angular application. Instead of having to write the code you just wrote into every method call, Angular allows you to create an HTTP Interceptor
class to place custom headers into each Web API call. Open a command prompt in the PTC
project and enter the following command to create a module, and have it registered in the app.module.
ng g m security/httpInterceptor -m
app.module --flat
Open the newly created file, http-interceptor.module.ts
, and replace all of the code with the code shown in Listing 11. All requests that are sent to a Web API
method using the HttpClient are now routed through this interceptor
class.
Listing 11: Add an HTTP Interceptor class to add a header to each Web API call
import { Injectable, NgModule } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@Injectable()
export class HttpRequestInterceptor
implements HttpInterceptor {
intercept(req: HttpRequest<any>,next: HttpHandler): Observable<HttpEvent<any>> {
var token = localStorage.getItem("bearerToken");
if(token) {const newReq = req.clone( {
headers: req.headers.set('Authorization','Bearer ' + token)
});
return next.handle(newReq);
}
else {
return next.handle(req);
}
}
};
@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true }
]
})
export class HttpInterceptorModule { }
In the intercept()
method, you retrieve the bearer token from local storage, and if the token exists, you clone the existing request that's being sent. You then create a new header on this request setting the headers
property with the Authorization header followed by the word "Bearer " and the token after this word. This is the new Web request that's returned from this method. If the token doesn't exist in local storage, the original Web request is returned. You can read more about HTTP Interceptors at https://angular.io/guide/http#intercepting-requests-and-responses.
You now finally see the reason why you stored the bearer token in local storage in the login()
method of the SecurityService
class. You might be wondering why you don't just inject a SecurityService
object into this class and retrieve it from the SecurityService
object like you do in all the other components. The reason is that you can't have an HTTP interceptor injected with any class that uses HttpClient as this causes a recursion error.
Open the product.service.ts
file and change the getProducts()
method back to what you had originally. Remove the code that creates the HttpHeaders, and pass this as a second parameter to the get()
method. The getProducts()
method should now look like the following.
getProducts(): Observable<Product[]> { return this.http.get<Product[]>(API_URL); }
Try It Out
Save your changes, go to your browser, log in as “psheriff”, and try accessing the Products menu. You should still be getting data from the product controller. This verifies that the HTTP Interceptor
class is working.
Add Security Policy
The [Authorize]
attribute ensures that a user is authenticated. However, for some Web API methods, you may wish to restrict access based on the claims you created. This is accomplished by adding authorization to the services of the .NET Core Web API project.
Open the Startup.cs
file and add the following code in the ConfigureServices
method, just under the code you added earlier for adding authentication. For each property you have in your AppUserAuth
object, you can add policy objects and specify the value the user must have.
services.AddAuthorization(cfg =>
{
// The claim key and value are case-sensitive
cfg.AddPolicy("CanAccessProducts", p =>
p.RequireClaim("CanAccessProducts", "true"));
});
Once you create your claims, you may add the Policy
property to the [Authorize]
attribute to check for any of the claim names. For example, on the ProductController.Get()
method, you can add the following code to the [Authorize]
attribute to restrict usage on this method to only those users whose CanAccessProducts
property is set to true
.
[Authorize(Policy = "CanAccessProducts")]
Try It Out
Open SQL Server and open an Edit window for the UserClaim table. Modify the claim CanAccessProducts
to a false
value for the “PSheriff” user. Open the app-routing.module.ts
file and remove the route guard for the products
path. Your route for the products path should now look like the following code snippet.
{
path: 'products', component: ProductListComponent
},
Save your changes, go back to the browser, and login as “psheriff”. Type http://localhost:4200/products
directly into the address bar and you should now get a 401-Unauthorized error. Put the route guard back and reset the CanAccessProducts
claim to a true
value in the UserClaim table.
Summary
In this article, you built Web API calls to authenticate users and provide an authorization
object back to Angular. In addition, you configured your .NET Core Web API project to use the JSON Web Token system to secure your Web API calls. You learned to send bearer tokens to Angular and have Angular send those tokens back using an HTTP Interceptor
class. In the next article, you'll learn to use an array of claims to secure your Angular application. To accomplish this, you build a custom structural directive in Angular.