JSON Web Token Authentication with ASP.NET Core 3.1

Evandro Gomes
11 min readJul 31, 2018

--

Photo provided by Pexels

This article approaches the implementation of authentication and authorization via JSON Web Token through an API built with ASP.NET Core 3.1, developed from scratch.

Overview

Authentication and authorization are primary requisites for the most part of modern applications.

The popularization of API development to serve different kinds of client applications (such as single page applications and mobile apps) brought the necessity to deliver security requisites using decentralized technologies and patterns.

The JSON Web Token specification was created to meet these needs, allowing systems of different platforms to exchange information and validate permissions.

What exactly is JSON Web Token?

JSON Web Token is a specification defined in RFC 7519, which defines a secure way for data transfer using JSON objects. JSON Web Tokens secure information by using cryptography algorithms.

These tokens are sent in headers of HTTP requests to access protected API endpoints.

What are the parts of a token?

A token is composed of three parts:

1. Header: generally consists of two properties:

o “alg”: the cryptography algorithm used to sign the token, ensuring its integrity;

o “typ”: the type of token.

{
“alg”: “HS256”,
“typ”: “JWT”
}

2. Payload: part of a token which contains Claims, or “assertions”, related to a user, and default claims contained in every token.

Basically, it refers to all user data needed for client applications and the API to execute validations related to user permissions, as well as claims needed to check if a token is valid and its data integrity.

There are some standard claims that almost all tokens have to implement:

o “jti”: represents a unique identifier for a token;

o “aud”: the audience, that is, for whom this token is intended for (in this case, the application that is requesting a token);

o “iss”: The token issuer, it means, the application that is emitting the token (in this case, the API);

o “exp”: expiration date;

o “nbf”: it means “not before”. It indicates that the token expiration date must be greater than or equal the specified date to be valid.

There are other defaults claims that you can check in the RFC.

{   "jti": "198f3849-ce23-4876-b1bd-bc9c936eec76",   "sub": "test@test.com",   "http://schemas.microsoft.com/ws/2008/06/identity/claims/role":    "Common",   "nbf": 1525268259,   "exp": 1525268289,   "iss": " JWPAPI ",   "aud": "SampleAudience"}

3. Signature: digital signature with the intent of validating and verifying tokens integrity. It is composed of the header data, payload, a secret key and the specified cryptography algorithm.

If a hacker gets the token and changes any data, the token will become invalid, since the signature will not match the expected one for the token.

HMACSHA256(   base64UrlEncode(header) + "." +   base64UrlEncode(payload),   secret)

These three parts are encoded as a base 64 string and joined using dots, allowing the resulting data to be sent through HTTP request headers.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxOThmMzg0OS1jZTIzLTQ4NzYtYjFiZC1iYzljOTM2ZWVjNzYiLCJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQ29tbW9uIiwibmJmIjoxNTI1MjY4MjU5LCJleHAiOjE1MjUyNjgyODksImlzcyI6IlNhbXBsZUlzc3VlciIsImF1ZCI6IkpXUEFQSSJ9.qGImJxUkWzale_IHgIC5s0WRWyetZsShnMuNErXKUe4

Development Approach in ASP.NET Core

The process of creating an API to meet security requisites via tokens is simple when using ASP.NET Core 3.1. The framework brings support for token generation and validation through various namespaces.

Source Code of the Example API

The complete source code is on GitHub. Click the link below to check it:

Clone the repository to test all API endpoints. There is documentation in the READMEfile of the repository to show examples of how to test all endpoints.

Notice: the first version of the API was written for ASP.NET Core 2. There were some updates recently to bring it to ASP.NET Core 3. Take a look at the releases section of the Github repository if you want to check the previous versions.

Application Scope

The API implements the following functionalities:

  • Simple user registration, allowing a user to have a unique login, a password and its roles (or permissions);
  • Access token generation;
  • Token refresh, to create new tokens when access tokens expire;
  • Access token revoking;
  • Permission validation using roles.

Frameworks and Libraries

In addition to ASP.NET Core 3.1, the API depends on the following frameworks and libraries:

  • Entity Framework Core (to store users data into a database);
  • AutoMapper (to map API resources to domain entities);
  • Entity Framework InMemory provider (just for testing, instead of using SQL Server, for instance).

Starting by Users Registration

The application users have roles, which represent their access permissions.

The API has two predefined roles: Common, which validates common users’ permissions, and Administrator, that represents system administrators’ permissions, this way simulating a real-world application. When a new user is created, they automatically receive the common users’ role.

The user registration is achieved through the following API endpoint:

/api/users

You can send a POST request to this endpoint specifying valid credentials (e-mail and password) in order to store the user data into a database.

The API has two predefined users to test access to protected API resources, one with common user permissions and other with administrator permissions.

The following JSON data can be used to test protected endpoints:

{   "email": "common@common.com",   "password": "12345678"},{   "email": "admin@admin.com",   "password": "12345678"}

The API endpoint matches the above method, from UsersControllerclass.

The API receives a request for this endpoint and the credentials are mapped to an instance of Userdomain class. Then, the user data is sent to the user service.

The method for user creation, from IUserServiceinterface, consists of two steps:

  • Verifying if any existing user corresponds to the specified e-mail;
  • Creating a password hash, through the implementation of IPasswordHasherinterface.

The hash generation assures that all passwords will be stored in a safe way into the database. This process avoids hackers and malicious users to retrieve users’ real passwords.

The code for creating and verifying password hashes was based on the implementation of the password hasher of ASP.NET Identity. I designed the functionality based on this issue.

Access Token Generation

Access tokens are tokens that contain claims used by the API to validate specific user data, as well as client applications data. They are sent in HTTP requests headers.

The example API generates access tokens that contain the following data:

  • An encoded token that can be used to access protected API endpoints, which contains an expiration date (in this case, 30 seconds after creating a token) and a payload with user claims, as well as valid audience and issuer;
  • A refresh token, which consists of an encoded token with an expiration date posterior to the access token’s expiration date (in this example, 60 seconds), that allows the client application to request a new access token when a valid one expires;
  • The expiration date of the access token (just for client validation purposes).

To get an access token, make a POST request to the following API endpoint sending the user credentials on the request body:

/api/login

The authentication service uses an implementation of ITokenHandler interface to generate tokens. This interface defines methods to create access tokens, to get refresh tokens and to revoke refresh tokens (these last two functionalities are going to be explained in a while).

The service also has a dependency on IPasswordHasherfor user credentials validation.

The token handler uses some configuration from appsettings.json with user data to generate the tokens.

You can see how the audience, issuer and expiration time for access tokens and for refresh tokens are defined in this section.

The expiration time is set in seconds. These configurations are loaded into an instance of TokenOptionsclass, via ASP.NET Core dependency injection mechanism.

Here is the implementation of the method for generating access tokens:

First, the handler generates a refresh token, which consists of a random hash and a date higher than the access token expiration date.

Then, an instance of JwtSecurityTokenclass generates the access token, and an instance of JwtSecurityTokenHandlercreates an encoded string to represent our JSON object. Both classes are part of System.IdentityModel.Tokens.Jwt namespace.

The claimsparameter gets the result of GetClaimsmethod, responsible for generating the following claims:

  • A claim with the token identifier;
  • Another one with the user’s email;
  • Claims representing the user permissions (roles).

Notice also the parameter signingCredentials. This parameter represents the token signature, which is needed to validate the token.

An instance of SigningCredentials, from Microsoft.IdentityModel.Tokens, is responsible for generating the signature.

An instance of SigningConfigurationsencapsulates the signing credentials.

The initialization of this class happens during application startup. It defines the following properties:

  • Key: security key for validating token signatures. This class is part of the Microsoft.IdentityModel.Tokens namespace. The key consists of a 2048 bits security key;
  • SigningCredentials: the token signature. The credentials are generated using the security key and an instance of the RSA SHA 256 algorithm. It is possible to use a different algorithm, like HMAC, for instance.

Coming back to the token generation process, the next step consists of adding the generated refresh token into a set that contains all valid refresh tokens. This set is checked when the API receives a request for new access tokens, to check if a refresh token is still valid.

In a real-world application, keeping refresh tokens in memory within a set is not a viable solution (except if you use some distributed caching strategy and tool, such as Redis).

A common approach is to store refresh tokens in a database. For the scope of this example, this is not necessary, since we will just have a handful of incoming requests for tokens.

Caution: be sure to encrypt refresh tokens if you’re going to store them in a database. Letting then uncrypted is a big security issue.

Finally, the API creates an instance of the AccessTokenclass and returns it to the controller, which maps the access token to a resource class.

Accessing protected API endpoints

Once you have a valid token in hands, you can access protected API resources.

There are two protected endpoints for testing this functionality.

/api/protectedforcommonusers

The above endpoint returns a simple string for any authenticated user (it means that the request must have a valid access token in its header).

/api/protectedforadministrators

The second endpoint returns a simple string only if the authenticated user is an administrator.

The endpoints validate permissions through the use of the Authorize attribute.

The ASP.NET Core pipeline executes validation over permissions using middlewares and security services defined in Startup.cs.

First, the API loads token configuration options from appsettings.json.

Then, the token signature class is loaded and registered for dependency injection as a singleton.

Finally, there are calls to the extension methods AddAuthentication, which is setting the default authentication scheme to JSON Web Tokens scheme, and AddJwtBeares, responsible for configuring token validation.

The validation parameters come from TokenValidationParametersclass, which is part of Microsoft.IdentityModel.Tokens namespace. The following parameters are configured here:

  • ValidAudience: indicates that the application which is sending requests to our API must be the same specified in appsettings.json;
  • ValidIssuer: indicates that the token issuer must correspond to the one specified in appsettings.json;
  • ValidateIssuerSigningKey: tells the API to validate the security key of tokens coming from the issuer application;
  • IssuerSigningKey: security key for signing tokens;
  • ValidateAudience: tells our API to check if the application which is consuming our tokens is the same as the one defined in appsettings.json;
  • ValidateLifetime: indicates that tokens expiration dates need to be verified;
  • ClockSkew: valid delay time to check if a token is valid or not.

For token validations to correctly work, it is necessary to use UseAuthenticationmiddleware before the middleware that configures routes for our application.

To access the protected endpoints, you have to add the following header to your requests:

Authorization: Bearer valid_access_token

Imagine that you have requested a token for a user that is part of the Common role. If you specify the authentication header as above and try to get a response from the endpoint /api/protectedforcommonusers, you will receive a response with 200 status (OK), as well as a string in the response body.

Requesting data from the common users’ endpoint.

If you try to use the same token to get data from the administrators’ endpoint, you will receive a response with 403 status (forbidden), which indicates that you do not have access to this content.

Receiving a 403 (forbidden) response from the administrators endpoint.

If you try to get data from the administrators’ endpoint sending a valid administrator access token in the request header, the API will send a response with 200 status (Ok) and a string in the response body.

Receiving data as an administrator.

If you request data from one of these endpoints sending an invalid token (an expired one of a token with characters changed by someone) or sending a request without specifying a token in the request header, you will receive a response with status 401 (unauthorized).

Receiving a 401 (unauthorized) response when trying to get administrators data.

Refresh Tokens

Now imagine that you are developing a single page application or mobile app, and you do not want that users have to sign in every time their “section” expires (in this case, every time an access token expires). To avoid this problem, it is possible to use a valid refresh token to request a new access token from the API, behind the scenes.

To request a new access token from a refresh token, make a POST request to /api/token/refresh. In the request body, you need to specify a valid refresh token, which will be used by the API to create a new access token.

The authentication service implements the RefreshTokenAsyncmethod, which receives a refresh token and the user email as parameters.

These parameters are necessary to correct generate claims for the new access token.

First, this method requests the complete refresh token from the token handler, which checks if the refresh token exists in the refresh tokens set and removes it from there before returning it to our authentication service (after all, we do not want that refresh tokens can be used multiple times).

Then, the method validates if the refresh token really exists (if it is contained in the refresh tokens set), if it has not expired and if the user email is valid.

If everything goes ok, our API generates a new access token and returns it according to the user data.

Revoking refresh tokens

Coming back to our automatic access token request process, imagine that now you have to create the sign out functionality. When the user signs out, it may happen that our API still has a valid corresponding refresh token on the token set. We need a way to revoke this token to avoid security issues.

The API has the /api/token/revoke endpoint. Sending a POST request to this endpoint with a valid refresh token on the request body causes our API to call RevokeRefreshTokenmethod. This method calls the token handler method of same name, which removes the token from the set.

Conclusion

Implementing authentication and authorization via tokens in ASP.NET Core application is easy when you use the new token generation and validation mechanism.

I created this example with the intent of helping people who don’t know what JSON Web Token is or people that have doubts on how to implement authentication and authorization via API, targeting multiple client applications.

To implement this solution in production, keep in mind you would have to change the refresh token implementation to use a different storage mechanism and to handle concurrency. You also should use user secrets as a way to store token configurations instead of appsettings.json. Also, consider encrypting user passwords when sending data to the API.

--

--