swirl
Home Software Blog Wallpapers Webtools
Using JWT authentication in ASP.NET Core Web API
Sunday 20, December 2020   |   Post link
Logo

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_n1JxgH4NHNWbHWq17miVNcDEuS9Kc
The 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.

Authentication flow

Protecting the weather forecast route

The Get() method is protected by adding an Authorize attribute.
[HttpGet]
[Authorize]
public IEnumerable Get()
{
	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..

Unauthorized

The output is the 401 (unauthorized) status code.

Authenticate

Let's authenticate using a username and password

Authenticate

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.

Authenticate

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




Comments

Posts By Year

2021 (4)
2020 (12)
2019 (6)
2018 (8)
2017 (11)
2016 (6)
2015 (18)
2014 (2)
2013 (4)
2012 (2)

Posts By Category

.NET (3)
.NET Core (2)
ASP.NET MVC (4)
AWS (3)
AWS API Gateway (1)
Android (1)
Architecture (1)
Azure (2)
Book review (3)
Business (1)
C# (3)
C++ (2)
CloudHSM (1)
Containers (3)
Corporate culture (1)
Database (3)
Database migration (1)
Desktop (1)
DotNet (2)
DotNet Core (2)
Entity Framework (3)
Git (2)
IIS (1)
JDBC (1)
Java (6)
Learning (1)
Life (6)
Linux (1)
Lucene (1)
Multi-threading (1)
OData (1)
Office (1)
PHP (1)
PowerShell (2)
Programming (23)
Rants (5)
SQL (2)
SQL Server (1)
Security (1)
Software Engineering (1)
Software development (1)
Solr (1)
Sql Server (2)
Storage (1)
T-SQL (1)
TDD (1)
TSQL (5)
Tablet (1)
Technology (1)
Test Driven (1)
Unit Testing (1)
Unit Tests (1)
Utilities (2)
VC++ (1)
VMWare (1)
VSCode (1)
Visual Studio (2)
Wallpapers (1)
Web API (2)
Win32 (1)
Windows (9)
XML (2)

Posts By Tags

.NET(5) API Gateway(1) ASP.NET(4) AWS(1) Adults(1) Advertising(1) Android(1) Anti-forgery(1) Authentication(2) Azure(2) Backup(1) Beliefs(1) BlockingQueue(1) Book review(2) Books(1) Busy(1) C#(4) C++(3) CLR(1) CORS(1) CSRF(1) CTE(1) Callbacks(1) Checkbox(1) CloudHSM(1) Cmdlet(1) Commons(1) Company culture(1) Complexity(1) Consumer(1) Consumerism(1) Containers(3) Core(2) Custom(2) DPI(1) Data-time(1) Database(4) Debugging(1) Delegates(1) Developer(2) Dockers(2) DotNetCore(3) EF 1.0(1) Encrypted(1) Entity framework(1) Events(1) File copy(1) File history(1) Font(1) Git(2) GradleApache(1) HierarchyID(1) IIS(1) Installing(1) Intelli J(1) JDBC(1) JSON(1) JUnit(1) JWT(1) Java(1) JavaScript(1) LinkedIn(1) Linux(1) Localization(1) Log4J(1) Lucene(1) MVC(4) Management(2) Migration history(1) Mobile Apps(1) Modern Life(1) Money(1) NGINX(1) NTFS(1) NUnit(1) OData(1) OPENXML(1) Objects(1) Office(1) Organization(1) PHP(1) Paths(1) PowerShell(2) Producer(1) Programming(1) Python(1) Quality(1) REDIS(2) Runtimes(1) S3-Select(1) SD card(1) SQL(2) SQL Code-first Migration(1) SSH(1) Sattelite assemblies(1) School(1) Secrets Manager(1) Self reliance(1) Shell(1) Solr(1) Sony VAIO(1) Spirituality(1) Sql Express(1) System Image(1) TDD(1) TSQL(3) Table variables(1) Tables(1) Tablet(1) Url rewrite(1) VMWare(1) VSCode(1) Validation(2) Wallpaper(1) Wallpapers(1) Web Development(4) Windows(2) Windows 10(2) Windows 2016(2) Windows 8.1(1) Work culture(1) XML(1) Yii(1)