Overview
This blog posts discusses JSON Web Tokens and how they can be used for securing your Asp.NET Core Web API application.
Recap the basics
We all know this but let's get this out of the way first - authentication is the proces of determing whothe person is or determining if the user is really who he says he is. Authorization is determining if the user is allowed to perform a certain operation.
Most systems are now API driven using REST and its important to ensure only the right users are allowed accessed to them. Services which expose APIs are also hosted redundantly to allow scaling and provide fault tolerance. This means a client who has authenticated himself with one endpoint should be allowed in by a another instance of the service also. Session based authentication will not work unless the session token is stored in a shared location like a database. JWT (Json Web Tokens) fit in very well in such scenarios.
How a JWT look like
Let's first see how a Json Web Token look like. Here is an example of a JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6InNpZGRoYXJ0aCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvZXhwaXJhdGlvbiI6IlN1bmRheSwgRGVjZW1iZXIgMjAsIDIwMjAiLCJleHAiOjE2MDg0ODE0ODQsImlzcyI6Ik5FVVRST04iLCJhdWQiOiJORVVUUk9OIn0.NT_1MrfKucC2M_n1JxgH4NHNWbHWq17miVNcDEuS9Kc
If you notice carefully there are three parts each separated by a dot. The parts are:
Part 1: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 Part 2: eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6InNpZGRoYXJ0aCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvZXhwaXJhdGlvbiI6IlN1bmRheSwgRGVjZW1iZXIgMjAsIDIwMjAiLCJleHAiOjE2MDg0ODE0ODQsImlzcyI6Ik5FVVRST04iLCJhdWQiOiJORVVUUk9OIn0 Part 3: NT_1MrfKucC2M_n1JxgH4NHNWbHWq17miVNcDEuS9Kc
Each part is base64 encoded so if we decode each part individually, this is what we get:
Part 1
{ "alg":"HS256", "typ":"JWT" }
The first part lets us know the algorithm used to hash the content of Part 1 and Part 2.
Part 2
{ "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier":"siddharth", "http://schemas.microsoft.com/ws/2008/06/identity/claims/expiration":"Sunday, December 20, 2020", "exp":1608481484, "iss":"NEUTRON", "aud":"NEUTRON" }
This part contains the list of claims. There are three types of claims: registered claims, public claims & private claims. Here is a great website to learn more about these.
Part 3
NT_1MrfKucC2M_n1JxgH4NHNWbHWq17miVNcDEuS9KcThe last part is the hash calculated using the content of Part1, Part2 and a secret key. This hash allows the service which reads to token to ensure that it has not been tampered. How? Because the hash is generated using a formula like:
hash = fn(base64(Part 1) + "." + base64(Part 2), apply a secret key)
The secret key is known only to the service. It a token generated by one service is to be honored by other services, this responsibility can be delegated to a separate service and make use of RSA public / private jey pair or the secret key needs to be available to all the services. This sample code does not use RSA but uses a SHA256 HMAC.
How it works
Similar to the previous post about Custom authentication schemes in ASP.NET Core, the REST service features an endpoint '/Authenticate' to authenticate a client. The other endpoint is the WeatherForecast endpoint which requires the caller to be authenticated. It returns weather forecast data.
The authentication flow is depicted in the image below.
Protecting the weather forecast route
The Get() method is protected by adding an Authorize attribute.[HttpGet] [Authorize] public IEnumerableGet() { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); }
The important thing to note is we are specifying 'Authorize' attribute to ensure only authorized calls are allowed access to the endpoint.
Plugging in the JWT authentication into the pipeline
We need to add Nuget package references to Microsoft.AspNetCore.Authentication.JwtBearer and System.IdentityModel.Tokens.Jwt to use the classes to work with Json Web Tokens. The JWTTokenService class is used to validate JWTs. This class is added as a middleware singleton service. The AddAuthentication method is then called for setting up the JWT authentication in the pipeline.
public void ConfigureServices(IServiceCollection services) { JWTTokenService tokenService = new JWTTokenService(Configuration); services.AddControllers(); services.AddSingleton<ITokenService>(tokenService); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.SaveToken = true; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = Environment.MachineName, ValidAudience = Environment.MachineName, IssuerSigningKey = tokenService.GetSecurityKey(), ClockSkew = TimeSpan.Zero }; }); }
The authenticaton endpoint
The /Authenticate endpoint is used to authenticate a client. The AuthenticationController class provides a Post method which accepts the username and password, it forwards this information to the JWTTokenService class which checks if the username and password matches. The actual list of username and passwords are stored in the appsetting.json file. Needless to say you can implement any scheme for persisting user information e.g. a database. Note, the passwords are stored as SHA256 hashes. You can use the hash utility to generate SHA256 hashes. Since passwords are being sent over HTTP traffic, never use plain HTTP, always use HTTPS.
The token service
The JWTTokenService class contains all the logic required for:
- Reading the users from the application settings file (appsettings.json)
- Checking if a username, password combination exists
- Creating & returning the JWT token
The JWT is signed using a secret key. This secret needs to be stored in as part of configuration such that it is not easily leaked. Using a key management service would be good idea. In this sample, the key is generated afresh each time the API service starts using the InitializeCrypto() method of the JWTTokenService class, this means tokens generated will not work if the service restarts.
using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using System.Collections.Generic; using System.Linq; namespace AuthenticatedWebApi.Security { public class JWTTokenService : ITokenService { public JWTTokenService(IConfiguration configuration) { _config = configuration; InitializeCrypto(); } public bool Authenticate(string user, string password, out string token) { bool result = false; token = null; List<Credential> users = _config.GetSection("Users").Get<List<Credential>>(); if (users.Where(u => u.Username == user && u.Password == password).Count() > 0) { string issuer = Environment.MachineName; SigningCredentials credentials = new SigningCredentials(_securityKey, SecurityAlgorithms.HmacSha256); DateTime expiresOn = DateTime.Now.AddMinutes(VALID_FOR_MINUTES); Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user), new Claim(ClaimTypes.Expiration, expiresOn.ToLongDateString()) }; JwtSecurityToken jwtToken = new JwtSecurityToken(issuer, issuer, claims, null, expiresOn, credentials); token = new JwtSecurityTokenHandler().WriteToken(jwtToken); result = true; } return result; } public AuthenticationTicket ValidateToken(string token) { return null; } public Microsoft.IdentityModel.Tokens.SymmetricSecurityKey GetSecurityKey() { return _securityKey; } private void InitializeCrypto() { RNGCryptoServiceProvider cryptoProvider = new RNGCryptoServiceProvider(); byte[] randomBytes = new byte[KEY_SIZE]; cryptoProvider.GetBytes(randomBytes, 0, KEY_SIZE); _securityKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(randomBytes); } private IConfiguration _config; private const int KEY_SIZE = 32; private Microsoft.IdentityModel.Tokens.SymmetricSecurityKey _securityKey; private const double VALID_FOR_MINUTES = 60.0; } }
Using the APIs
Download the code from GitHub. Build and run the service. We'll use the curl program to invoke the APIs, you can use other tools like Postman if you like.
Access the API without authentication.
Let's see what happens if one accesses the API without having authenticated ourselves..
The output is the 401 (unauthorized) status code.
Authenticate
Let's authenticate using a username and password
The result is 200 (OK) and the response is the JWT string.
Access the API with the token
Let's now access the weather-forecast API and specify the JWT using the Authentication header.
The result is 200 (OK) and the response is the weather forecast.
Conclusion
JWTs is a convenient way to secure your REST APIs. DotNetCore provides everything you need to not just validate JWTs but also create them based on your authentication logic.
References
- One of the best places to learn about JWT
- List of JWT claims
- Related blog post covering custom authentication
- Source code on GitHub