AspNet - How To Use JWT-based Authentication

Getting Started

First take a look at the How To Create JWTokens guide I created as I am going to be basing this section off the choices made in that guide.

Anticipate the amount of time to wire everything up and understand whats going on is about an hour. After you get it, it's pretty much copy and paste.

Why Http Handlers

They are quick and easy. That's the gist of it. They aren't too complicated to understand as long as you get the order right and remember to register them to your WebApiConfig.

I am going to create a fresh Web.Api project (Asp.Net). These were the project template creation choices for Visual Studio 2017 (latest version as of 3/31/2019).

NuGet Dependencies

Microsoft.IdentityModel.Tokens System.IdentityModel.Tokens.Jwt Newtonsoft

Next we need to create a base handler class for our Web.Api project.

For this example we will call ours an AuthenticationHandler.cs and put it in a folder and namespace called Handlers.

using System.Net.Http; namespace WebApiJwtDemo.Handlers { internal class AuthenticationHandler : DelegatingHandler { // Currently showing an error. } }

Our Handler will need to have a method SendAsync override.

using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace WebApiJwtDemo.Handlers { internal class AuthenticationHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return await base.SendAsync(request, cancellationToken); } } }

Right now, this is passing requests to the base.SendAsync and returning the HttpResponseMessage. You want to view this as we are injecting a step into the request flow.

Knowing that, we definitely want this to be the first Handler in our Api, or least the handler above any step that requires authentication after this point (routing). Some people like to log requests that come in first, then authenticate, i.e you could having a Logging Handler step in front of this but that is because it doesn't need authentication for its logic.

Read Tokens

using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace WebApiJwtDemo.Handlers { internal class AuthenticationHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var response = new HttpResponseMessage(); var tokenExists = TokenExists(request, out string token); //await base.SendAsync(request, cancellationToken); return response; } private bool TokenExists(HttpRequestMessage request, out string token) { var tokenFound = false; token = null; if (request.Headers.TryGetValues("Authorization", out IEnumerable<string> authHeaders) && authHeaders.Any()) { var bearerToken = authHeaders.ElementAt(0); // Authorization Header Flexibility // Authorization value start with "Bearer " then trim it off, else treat value as Token. token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken; tokenFound = true; } return tokenFound; } } }

I have commented out the base.SendAsync because it isn't where I really want it yet.

In this section we check for an Authorization header existing. If it does and has a value, let's try and parse it out to a string called token.

An extra step I have added here is if the keyword "bearer" was on the value side with a space, we remove it first. This is simply flexibility.

Now take note, Jwt needs to be in the "Authorization" header key name because we are looking for "Authorization". It could have been called "ZaphodBeeblebroxSsn" so if you want to go with a different keyword for some extra obscurity - have at it.

Valid Token Logic

using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace WebApiJwtDemo.Handlers { internal class AuthenticationHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var validToken = false; var response = new HttpResponseMessage(); if (TokenExists(request, out string token)) { try { validToken = await ValidateTokenAsync(token); } // This section can be greatly expanded to give more custom error messages. catch (SecurityTokenExpiredException) { /* Handle */ } catch (SecurityTokenValidationException) { /* Handle */ } catch (SecurityTokenException stex) { /* Handle */ } catch (Exception ex) { /* Handle */ } if (validToken) // Happy Path For Valid JWT { response = await base.SendAsync(request, cancellationToken); } else { response.StatusCode = HttpStatusCode.Unauthorized; } } else { response.StatusCode = HttpStatusCode.Unauthorized; // Go in Anonymous response = await base.SendAsync(request, cancellationToken); } return response; } private bool TokenExists(HttpRequestMessage request, out string token) { var tokenFound = false; token = null; if (request.Headers.TryGetValues("Authorization", out IEnumerable<string> authHeaders) && authHeaders.Any()) { var bearerToken = authHeaders.ElementAt(0); // Authorization Header Flexibility // Authorization value start with "Bearer " then trim it off, else treat value as Token. token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken; tokenFound = true; } return tokenFound; } private async Task<bool> ValidateTokenAsync(string token) { return await Task.FromResult(true); } } }

It's really starting to come together at this point. The only thing we really have to wire up next is ValidateTokenAsync properly.

Actually Validating Tokens

For the next section we are going to focus only on validating code.

private async Task<bool> ValidateTokenAsync(string token) { var userIsValid = true; // assumed user is good (but could be false) var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); var tvp = GetTokenValidationParameters(); //Extract and assigns the PrincipalId from JWT User/Claims. HttpContext.Current.User = Thread.CurrentPrincipal = jwtSecurityTokenHandler.ValidateToken(token, tvp, out SecurityToken securityToken); // TODO: Extra Validate the UserId, check for user still exists, user isn't banned, user registered email etc. //userIsValid = await _userService.ValidateUserAsync(HttpContext.Current.User.GetUserId()); // Once a token is out in the wild (and since I assume you don't save them because thats normal) it's valid till it // expires. I highly encourage doing a quick stored procedure or EF check on the user's meta state. if (!userIsValid) throw new SecurityTokenValidationException(); return await Task.FromResult(userIsValid); } private TokenValidationParameters GetTokenValidationParameters() { TokenValidationParameters validationParameters = new TokenValidationParameters() { ValidAudience = "https://houseofcat.io", // these values derive from the Create JWT token guide (don't copy these!) ValidIssuer = "https://houseofcat.io", // these values derive from the Create JWT token guide (don't copy these!) ValidateLifetime = true, ValidateIssuerSigningKey = true, LifetimeValidator = this.LifetimeValidator, IssuerSigningKey = new SymmetricSecurityKey( Encoding.Default // these values derive from the Create JWT token guide (don't copy these!) .GetBytes("J6k2eVCTXDp5b97u6gNH5GaaqHDxCmzz2wv3PRPFRsuW2UavK8LGPRauC4VSeaetKTMtVmVzAC8fh8Psvp8PFybEvpYnULHfRpM8TA2an7GFehrLLvawVJdSRqh2unCnWehhh2SJMMg5bktRRapA8EGSgQUV8TCafqdSEHNWnGXTjjsMEjUpaxcADDNZLSYPMyPSfp6qe5LMcd5S9bXH97KeeMGyZTS2U8gp3LGk2kH4J4F3fsytfpe9H9qKwgjb")) }; return validationParameters; } // Customize this however you want for LifeTime validation. private bool LifetimeValidator( DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) { var valid = false; if ((expires.HasValue && DateTime.UtcNow < expires) && (notBefore.HasValue && DateTime.UtcNow > notBefore)) { valid = true; } return valid; }

We have added a TokenValidationParameters, a LifeTimeValidator, and the code inside ValidateTokenAsync.

Extra

// GetUserId() is an extension I wrote. userIsValid = await _userService.ValidateUserAsync(HttpContext.Current.User.GetUserId()); // This is how it could look to simplify UserId access. public static int GetUserId(this IPrincipal user) { var ci = user.Identity as ClaimsIdentity; var claim = ci.Claims.FirstOrDefault(c => c.Type == SchemaUserId); if (!int.TryParse(claim?.Value ?? string.Empty, out int userId)) { userId = -1; } // Negative one indicates error return userId; }

Everything Together In One Big Block

using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; namespace WebApiJwtDemo.Handlers { internal class AuthenticationHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var validToken = false; var response = new HttpResponseMessage(); if (TokenExists(request, out string token)) { try { validToken = await ValidateTokenAsync(token); } // This section can be greatly expanded to give more custom error messages. catch (SecurityTokenExpiredException) { /* Handle */ } catch (SecurityTokenValidationException) { /* Handle */ } catch (SecurityTokenException) { /* Handle */ } catch (Exception) { /* Handle */ } if (validToken) // Happy Path For Valid JWT { response = await base.SendAsync(request, cancellationToken); } else { response.StatusCode = HttpStatusCode.Unauthorized; } } else // Optional Public access as Anonymous { response.StatusCode = HttpStatusCode.Unauthorized; response = await base.SendAsync(request, cancellationToken); } return response; } private bool TokenExists(HttpRequestMessage request, out string token) { var tokenFound = false; token = null; if (request.Headers.TryGetValues("Authorization", out IEnumerable<string> authHeaders) && authHeaders.Any()) { var bearerToken = authHeaders.ElementAt(0); // Authorization Header Flexibility // Authorization value start with "Bearer " then trim it off, else treat value as Token. token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken; tokenFound = true; } return tokenFound; } private async Task<bool> ValidateTokenAsync(string token) { var userIsValid = true; // assumed user is good (but could be false) var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); var tvp = GetTokenValidationParameters(); //Extract and assigns the PrincipalId from JWT User/Claims. HttpContext.Current.User = Thread.CurrentPrincipal = jwtSecurityTokenHandler.ValidateToken(token, tvp, out SecurityToken securityToken); // TODO: Extra Validate the UserId, check for user still exists, user isn't banned, user registered email etc. //userIsValid = await _userService.ApiUserGetValidationAsync(HttpContext.Current.User.GetUserId()); // GetUserId() is an extension method I wrote so you will need to write something like //this for yourself. if (!userIsValid) throw new SecurityTokenValidationException(); return await Task.FromResult(userIsValid); } private TokenValidationParameters GetTokenValidationParameters() { // Cleanup return new TokenValidationParameters { ValidAudience = "https://houseofcat.io", // Remember to copy your own. ValidIssuer = "https://houseofcat.io", // Also don't leave them as magic strings, load them from the app.config. ValidateLifetime = true, ValidateIssuerSigningKey = true, LifetimeValidator = this.LifetimeValidator, IssuerSigningKey = new SymmetricSecurityKey( Encoding.Default // Seriously don't copy this string. It's bad for you. Stop. Go here: https://passwordsgenerator.net/ .GetBytes("J6k2eVCTXDp5b97u6gNH5GaaqHDxCmzz2wv3PRPFRsuW2UavK8LGPRauC4VSeaetKTMtVmVzAC8fh8Psvp8PFybEvpYnULHfRpM8TA2an7GFehrLLvawVJdSRqh2unCnWehhh2SJMMg5bktRRapA8EGSgQUV8TCafqdSEHNWnGXTjjsMEjUpaxcADDNZLSYPMyPSfp6qe5LMcd5S9bXH97KeeMGyZTS2U8gp3LGk2kH4J4F3fsytfpe9H9qKwgjb")) }; } private bool LifetimeValidator( DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) { var valid = false; // Additional checks can be performed on the SecurityToken or the validationParameters. if ((expires.HasValue && DateTime.UtcNow < expires) && (notBefore.HasValue && DateTime.UtcNow > notBefore)) { valid = true; } return valid; } } }

There you have it! A complex AuthenticationHandler for Identity based on JWTs.

Adding The AuthenticationHandler To The Api

using System.Web.Http; using WebApiJwtDemo.Handlers; namespace WebApiJwtDemo { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API configuration and services // Performs JWT Identity Validation config.MessageHandlers.Add(new AuthenticationHandler()); } } }

Once wired up, we can test it out!

Changing Value Controller To Require Authentication

using System.Collections.Generic; using System.Web.Http; namespace WebApiJwtDemo.Controllers { public class ValuesController : ApiController { // GET api/values [Authorize] // <- ADDED THIS! public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } } }

And now, we test with Postman against our Api running in debug!

Testing Without Authentication

Expected failure!

Testing With Authentication

So now we test again, but this time with a valid token in the "Authorization" header. We can use the token in this case from the How To Create JWTs guide.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImNhdEBob3VzZW9mY2F0LmlvIiwibmFtZWlkIjoiMSIsInVuaXF1ZV9uYW1lIjoiSG91c2UgQ2F0IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy91c2VyZGF0YSI6IntcIkFjY291bnRJZFwiOjF9IiwibmJmIjoxNTUzOTU5MDg4LCJleHAiOjE1NTQ1NjM4ODgsImlhdCI6MTU1Mzk1OTA4OCwiaXNzIjoiaHR0cHM6Ly9ob3VzZW9mY2F0LmlvIiwiYXVkIjoiaHR0cHM6Ly9ob3VzZW9mY2F0LmlvIn0.9UdREJIBCbyeymL0qFmSPN0YdGe_NMlgiYL0MVtLFbw

Bonus

If you were using roles from the previous How To Create JWTs guide, this is what Authorize should look like on a controller with Role authorization!

using System.Collections.Generic; using System.Web.Http; namespace WebApiJwtDemo.Controllers { public class ValuesController : ApiController { // GET api/values [Authorize(Roles = "Admin,Employee")] // <- ADDED THIS! public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } } }

Now, not only do you need to Authenticate succesfully, your user must also be in the Roles Admin and/or Employee.

Additional Information

© (2017-2020) HouseofCat.io - Rockin' NetCore 3.1.x, C# 8.0, HTTP2

Microsoft: Message Handlers, How Do They Even Work?!