This tutorial is built in .NET 6 & Angular 12
In this tutorial we will go through an example of how we can create an angular app which will consume .NET core web Api and we will authenticate that using JWT (JSON Web Token).
Authentication implementation brief
Here in this example, we are implementing authentication using JWT token and refresh token. On successful authentication our Api will return one short live JWT token which will expire in 10 minutes, and a refresh token that will expire in 5 days in an HTTP Only Cookie. With JWT token we are able to access any secure routes which are not publicly accessible; it can be accessed by only authenticated users. If any unauthenticated user tries to access that Api URL he will not be allowed to refresh the token we are using to generate new JWT token when they expire.
HTTP only cookie is used for refresh tokens to increase the security because they are not accessible to client-side JavaScript which prevent XSS (cross site scripting) attacks, and refresh tokens only have access to generate new JWT tokens via URL (/user/refresh-token route) which prevent them from being used in CSRF (cross site request forgery)
API Endpoints
This example .NET Api has below endpoints/routes to demonstrate authenticate with JWT, refreshing and revoking the tokens and accessing the secure routes.
/users/authenticate - this route will accept post request containing username and password in the body. On success of JWT access token is returned with basic user details, and an HTTP Only cookie containing refresh token.
/users/refresh-token – this route that accepts POST requests containing a cookie with refresh token. On success, a new JWT access token is returned with basic user details and an HTTP Only cookie containing a new refresh token.
/users/revoke-token – secure route that accepts POST request containing a refresh token either in request body or in a cookie, if both are priority is given to request body. On success the token is revoked and no longer will be used to generate JWT token.
/users- secure route that accepts GET requests and returns a list of all users in the application.
/users/{id}- secure route that accepts GET requests and returns the details of the user with related id.
/users/{id}/refresh-tokens – secure route that accepts GET requests and returns a list of all refresh token (active and revoked) of the user for a specific id.
Refresh Token Rotation
Each time a refresh token is used to generate a new JWT token via (/users/refresh-token route), the refresh token is revoked and removed by new refresh token, which makes it less likely that a compromised token will be valid. When a refresh token is rotated the new token is saved in the ReplaceByToken field of the revoked token to create an audit trial in the database.
Revoked and expired refresh token are kept in the database for a number of days set in RefreshTokenTTL property in appsetting.json file. The default is 2 days, after which old inactive token are deleted by user service in the Authenticate () and RefreshToken() methods.
Revoked Token Reuse Detection
If an attempt is made to generate a new JWT token using a revoked refresh token, the API treats this as a potentially malicious user with stolen (revoked) refresh token or a valid user is attempting to access the system after their token is revoked by malicious user or stolen token (active) refresh token.in either case Api revoke all descends tokens because the tokens and its descends where likely to be created on same device which may have been compromised. The reason revoked is recorded as “Attempt reuse of revoked ancestor token” so the user can see in database or via the /users/{id}/refresh-tokens route
EF Core InMemory database for testing
To keep the Api code as simple as possible, it's configured to use the EF Core InMemory database provider which allows Entity Framework Core to create and connect to an in-memory database rather than having to install a real db. server. A test user is automatically created on startup in the Program.cs file.
The db. provider can be easily switched out to connect a real database such as SQL Server, Oracle, MySQL etc.
Tools Required to run the .NET 6.0 Tutorial API Locally
Visual studio 2022 – To create and run the .NET 6 Project Currently visual studio 2022 supports .NET 6.
Before Running in Production
Before running in production make sure that you update the secret property in appsetting.json file, it is used to sign and verify the JWT token for authentication change it to random string to ensure that nobody can generate a JWT token with same secret and gain unauthorized access to you Api. A quick and easy way is to join a couple of GUIDs together to make a long random string (e.g., from https:www.guidgenerator.com)
Run an Angular app with the JWT Refresh Tokens API
For full details about the example Angular & .NET 6 application see the post GitHub - manektechgit/dot6-net-jwt-refresh-token-with-angular: An .Net6 refresh token and JWT token example how to use it using angular full code was in this repo.. But to get up and running quickly just follow the below steps.
- Install Node.js and npm from https://nodejs.org.
- Download or clone the Angular 12 tutorial code from https://github.com/manektechgit/dot6-net-jwt-refresh-token-with-angular.git
- Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
- Remove or comment out the line below the comment // provider used to create a fake backend located in the /src/app/app.module.ts file.
- Start the application by running npm start from the command line in the project root folder, this will launch a browser displaying the Angular example application and it should be hooked up with the .NET 6.0 JWT Refresh Tokens API that you already have running.
.NET 6.0 Tutorial Project Structure
The .NET tutorial project is organized into following folders
Authorization
Contains all the classes that are responsible for implementing custom JWT authentication and authorization in the Api.
Controllers
Define the endpoints / routes for the web Api, controllers are gateways for client applications via https requests.
Models
Represent request and response model for controller methods, request models define the parameters for incoming request and response models define the data that is returned.
Entities
Represent the application data that is stored in the database.
Entity framework Core (EF core) maps relational data from database to instances of C# entity objects to be used within the application for data management and CRUD operations.
Helpers
Anything that doesn't fit in the above folder.
Below is the project architecture
.NET Allows Anonymous Attribute
Path: /Authorization/AllowAnonymousAttribute.cs
The custom [AllowAnonymous] attribute is used to allow anonymous access to specified action methods of controllers that are decorated with the [Authorize] attribute. It's used in the user's controller to allow anonymous access to the authenticate and refresh-token action methods. The custom authorize attribute below skips authorization if the action method is decorated with [AllowAnonymous].
I created a custom allow anonymous (instead of using the built in one) for consistency and to avoid ambiguous reference errors between namespaces.
.NET Custom Authorize Attribute
Path: /Authorization/AuthorizeAttribute.cs
The custom [Authorize] attribute is used to restrict access to controllers or specified action methods. Only authorized requests are allowed to access action methods that are decorated with the [Authorize] attribute.
When a controller is decorated with the [Authorize] attribute all action methods in the controller are restricted to authorized requests, except for methods decorated with the custom [AllowAnonymous] attribute above.
Authorization is performed by the OnAuthorization method which checks if there is an authenticated user attached to the current request (context.HttpContext.Items["User"]). An authenticated user is attached by the custom jwt middleware if the request contains a valid JWT access token.
On successful authorization no action is taken and the request is passed through to the controller action method, if authorization fails a 401 Unauthorized response is returned.
.NET Custom JWT Middleware
Path: /Authorization/JwtMiddleware.cs
The custom JWT middleware extracts the JWT token from the request Authorization header (if there is one) and validates it with the jwtUtils.ValidateToken() method. If validation is successful the user id from the token is returned, and the authenticated user object is attached to the HttpContext.Items collection to make it accessible within the scope of the current request.
If token validation fails or there is no token, the request is only allowed to access public (anonymous) routes because there isn't an authenticated user object attached to the HTTP context. The authorization logic that checks if the user object is attached is located in the custom authorize attribute, and if authorization fails it returns a 401 Unauthorized response.
.NET JWT Utils
Path: /Authorization/JwtUtils.cs
The JWT utils class contains methods for generating and validating JWT tokens, and generating refresh tokens.
The GenerateJwtToken() method returns a short lived JWT token that expires after 15 minutes, it contains the id of the specified user as the "id" claim, meaning the token payload will contain the property "id": <userId> (e.g. "id": 1). The token is created with the JwtSecurityTokenHandler class and digitally signed using the secret key stored in the app settings file.
The ValidateJwtToken() method attempts to validate the provided JWT token and return the user id ("id") from the token claims. If validation fails null is returned.
The GenerateRefreshToken() method returns a new refresh token that expires after 7 days. The created date and user ip address are saved with the token to create an audit trail and to help identify any unusual activity. The local getUniqueToken() method returns a guaranteed unique refresh token string by verifying that it doesn't already exist in the database.
.NET Users Controller
Path: /Controllers/UsersController.cs
The .NET user's controller defines and handles all routes / endpoints for the api that relate to users, including authentication, refreshing and revoking tokens, and fetching users and refresh tokens. Within each route the controller calls the user service to perform the action required, this is done to keep the controller lean and to maintain a clean separation of concerns between the route handling code in the controller and the business logic + data access code in the service.
The controller actions are secured with JWT using the [Authorize] attribute, with the exception of the Authenticate and RefreshToken methods which allow public access by overriding the [Authorize] attribute on the controller with an [AllowAnonymous] attribute on each action method. I chose this approach so any new action methods added to the controller will be secure by default unless explicitly made public.
The setTokenCookie() helper method appends an HTTP Only cookie with a refresh token to the response. HTTP Only cookies are used to increase security because they are not accessible to client-side JavaScript which prevents XSS (cross site scripting) attacks, and refresh tokens only have access to generate new JWT tokens (via the /users/refresh-token route) which prevents them from being used in CSRF (cross site request forgery) attacks.
.NET Refresh Token Entity
Path: /Entities/RefreshToken.cs
The refresh token entity class represents the data stored in the database for refresh tokens.
Entity classes are also used to pass data between different parts of the application (e.g., between services and controllers) and can be used to return http response data from controller action methods.
The [Owned] attribute marks the refresh token class as an owned entity type, meaning it can only exist as a child / dependent of another entity class. In this example a refresh token is always owned by a user entity.
The [Key] attribute explicitly sets the Id property as the primary key in the refresh token database table, this usually is not required since Entity Framework automatically looks for a property named Id to set as the primary key, however the [Owned] attribute causes Entity Framework to create a composite primary key consisting of the id and owner id (refresh token id + user id) which can cause errors with auto generated id fields. Explicit use of the [Key] attribute prevents a composite primary key from being created by the [Owned] attribute.
The [JsonIgnore] attribute prevents the id from being serialized and returned with refresh token data in api responses.
.NET User Entity
Path: /Entities/User.cs
The user entity class represents the data stored in the database for users.
Entity classes are also used to pass data between different parts of the application (e.g., between services and controllers) and can be used to return http response data from controller action methods.
The [JsonIgnore] attribute prevents the PasswordHash and RefreshTokens properties from being serialized and returned with user data in Api responses. There is a dedicated route for fetching refresh token data (/users/{id}/refresh-tokens).
Path: /Helpers/AppException.cs
.NET Custom App Exception
The app exception is a custom exception class used to differentiate between handled and unhandled exceptions in the .NET API. Handled exceptions are generated by application code and used to return friendly error messages, for example business logic or validation exceptions caused by invalid request parameters, whereas unhandled exceptions are generated by the .NET framework or caused by bugs in application code.
.NET App Settings Class
Path: /Helpers/AppSettings.cs
The app settings class contains properties defined in the appsettings.json file and is used for accessing application settings via objects that are injected into classes using the .NET built in dependency injection (DI) system. For example, the user service accesses app settings via an IOptions<AppSettings> appSettings object that is injected into the constructor.
Mapping of configuration sections to classes is done in the Program.cs file.
.NET Data Context
Path: /Helpers/DataContext.cs The data context class is used for accessing application data through Entity Framework. It derives from the Entity Framework DbContext class and has a public Users property for accessing and managing user data. The data context is used by the user service for handling all low-level data (CRUD) operations. options.UseInMemoryDatabase() configures Entity Framework to create and connect to an in-memory database so the API can be tested without a real database, this can be easily updated to connect to a real db server such as SQL Server, Oracle, MySQL etc.
.NET Global Error Handler Middleware
Path: /Helpers/ErrorHandlerMiddleware.cs
The global error handler is used catch all errors and remove the need for duplicated error handling code throughout the .NET api. It's configured as middleware in the Porgram.cs file.
Errors of type AppException are treated as custom (app specific) errors that return a 400 Bad Request response, the .NET built-in KeyNotFoundException class is used to return 404 Not Found responses, all other exceptions are unhandled and return a 500 Internal Server Error response.
See the user service for examples of custom errors and not found errors thrown by the api.
.NET Authenticate Request Model
Path: /Models/Users/AuthenticateRequest.cs
The authenticate request model defines the parameters for incoming POST requests to the /users/authenticate route, it is attached to the route by setting it as the parameter to the Authenticate action method of the user's controller. When an HTTP POST request is received by the route, the data from the body is bound to an instance of the AuthenticateRequest class, validated and passed to the method.
.NET Data Annotations are used to automatically handle model validation, the [Required] attribute sets both the username and password as required fields so if either are missing a validation error message is returned from the api.
.NET Authenticate Response Model
Path: /Models/Users/AuthenticateResponse.cs
The authenticate response model defines the data returned by the Authenticate method of the user's controller after successful authentication. It includes basic user details, a JWT token and a refresh token.
The refresh token is decorated with the [JsonIgnore] attribute to prevent it from being returned in the response body because it is returned in an HTTP Only cookie. An HTTP Only cookie increases security because it is not accessible to client-side JavaScript which prevents XSS (cross site scripting), and a refresh token only has access to generate a new JWT token (via the /users/refresh-token route) which prevents it from being used in CSRF (cross site request forgery).
.NET Revoke Token Request Model
Path: /Models/Users/RevokeTokenRequest.cs
The revoke token request model defines the parameters for incoming POST requests to the /users/revoke-token route of the api, it is attached to the route by setting it as the parameter to the RevokeToken action method of the user's controller. When an HTTP POST request is received by the route, the data from the body is bound to an instance of the RevokeToken class, validated and passed to the method.
The Token property is optional in the request body because the route also supports revoking the token sent in the refreshToken cookie. If both are present priority is given to the token in the request body.
.NET User Service
Path: /Services/UserService.cs
The user service contains the core logic for authentication, generating JWT and refresh tokens, refreshing and revoking tokens, and fetching user data.
The top of the UserService.cs file contains the IUserService interface which defines the public methods for the user service, below the interface is the concrete UserService class that implements the interface.
The Authenticate () method finds a user by username and verifies the password against the hashed password in the database using BCrypt, and on success the user details are returned with a JWT and a refresh token. The RefreshToken() method accepts an active refresh token and returns the user details with a new JWT token and a new refresh token. The old refresh token (the one used to make the request) is revoked and can no longer be used, this technique is known as refresh token rotation and increases security by making refresh tokens short lived. When a refresh token is rotated the new token is saved in the ReplacedByToken field of the revoked token to create an audit trail in the database.
If an attempt is made to use a revoked refresh token, the API treats it as a potentially malicious user with a stolen (revoked) refresh token, or a valid user attempting to access the system after their token has been revoked by a malicious user with a stolen (active) refresh token. In either case the API revokes all descendant tokens because the token and its descendants were likely created on the same device which may have been compromised.
The RevokeToken() method accepts an active refresh token and revokes it so it can no longer be used. A token is revoked when it has a Revoked date. The source ip address of the request that revoked the token is saved in the RevokedByIp field.
The GetAll() method returns a list of all users in the system, and the GetById() method returns the user with the specified id.
Revoked and expired refresh tokens are kept in the database for the number of days set in the RefreshTokenTTL property in the appsettings.json file. The default is 2 days, after which old inactive (revoked or expired) tokens are deleted by the Authenticate() and RefreshToken() methods.
.NET Program
Path: /Program.cs
The .NET 6 Program file contains top-level statements which are converted by the new C# 10 compiler into a Main () method and class for the .NET program. The Main () method is the entry point for a .NET application, when an app is started it searches for the Main () method to begin execution. The top-level statements can be located anywhere in the project but are typically placed in the Program.cs file, only one file can contain top-level statements within a .NET application.
The Web Application class handles app startup, lifetime management, web server configuration and more. A WebApplicationBuilder is first created by calling the static method WebApplication.CreateBuilder(args), the builder is used to configure services for dependency injection (DI), a WebApplication instance is created by calling builder.Build (), the app instance is used to configure the HTTP request pipeline (middleware), then the app is started by calling app.Run().
I wrapped the add services... and configure HTTP... sections in curly brackets {} to group them together visually, the brackets are completely optional.