In this article we will cover the following,
- Creating the method to generate the JWT token
- Creating the middleware needed to validate the token
- Decorating the API controller
- Testing our API with Fiddler
First things first, let's start with a brand new project. I am using VS 2019 Community Edition.
Create a new ASP.NET Core Web Application. Choose the API with no authentication template.
I am calling my project AuthTest.API
IMPORTANT. I highly suggest you name your project, folders and classes the same as the article below, or you will find yourself having to track down and clean up the namespaces.
If you want the source code you can get it from
github.
Good! Now let's do some coding. I am adding two folders to the project: Services and Middleware just for organization purposes.
In the Services folder add a class called JwtServices.cs
We also need some Nuget packages. Right-click Dependencies -> Manage Nuget Packages... on the Browse tab search and install both of these packages:
- System.IdentityModel.Tokens.Jwt
- Microsoft.IdentityModel.Tokens
Let's fill in the JwtService class.
- using System;
- using System.Text;
- using System.Security.Claims;
- using Microsoft.IdentityModel.Tokens;
- using System.IdentityModel.Tokens.Jwt;
- using Microsoft.Extensions.Configuration;
-
- namespace AuthTest.API.Services
- {
- public class JwtService
- {
- private readonly string _secret;
- private readonly string _expDate;
-
- public JwtService(IConfiguration config)
- {
- _secret = config.GetSection("JwtConfig").GetSection("secret").Value;
- _expDate = config.GetSection("JwtConfig").GetSection("expirationInMinutes").Value;
- }
-
- public string GenerateSecurityToken(string email)
- {
- var tokenHandler = new JwtSecurityTokenHandler();
- var key = Encoding.ASCII.GetBytes(_secret);
- var tokenDescriptor = new SecurityTokenDescriptor
- {
- Subject = new ClaimsIdentity(new[]
- {
- new Claim(ClaimTypes.Email, email)
- }),
- Expires = DateTime.UtcNow.AddMinutes(double.Parse(_expDate)),
- SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
- };
-
- var token = tokenHandler.CreateToken(tokenDescriptor);
-
- return tokenHandler.WriteToken(token);
-
- }
- }
- }
In the construtor I am pulling some data out of the appsettings.json, but we haven't added those settings yet. Don't worry - we will right after this.
The reason for it is that the JWT generator needs some kind of secret string, some kind of password if you will, and an expiration date to generate the token.
The secret can be anything you want, just like a random password. I just typed in some random letters and numbers, and I decided the expiration is 1440 minutes (24hrs).
That means, the users for my API will have to get a new token every 24 hrs. It can be anything you want. You could choose to only expire the token if the user logs out (not recommended) or you could renew the token every so often. I am not covering that here.
Pay special attention to the Subject property line 27 through 30.
I am passing in an email to the function, GenerateSecurityToken(string email) and storing that email in the token. You could pass in some a user object GenerateSecurityToken(User user) for example and store a lot more information by adding new claims. This way you don't need to take trips to the DB to get that data, when the user makes a call into the system.
Example
Something like this...
- public string GenerateSecurityToken(User user){
- ...
- Subject = new ClaimsIdentity(new[]
- {
- new Claim(ClaimTypes.Email, user.Email),
- new Claim(ClaimTypes.Name, user.Name),
- new Claim(ClaimTypes.Role, user.Role),
- new Claim(ClaimTypes.DateOfBirth, user.DOB),
- })
- ...
- }
We went on a bit of a tangent, let's get back to the appsettings.config. In the appsettings.json we need to add a new configuration. This is what my appsettings.config looks like right now.
This is what it looks like after adding the JwtConfig section.
The JWT generation,
- {
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
- }
- },
- "AllowedHosts": "*",
- "JwtConfig": {
- "secret": "PDv7DrqznYL6nv7DrqzjnQYO9JxIsWdcjnQYL6nu0f",
- "expirationInMinutes": 1440
- }
- }
We now have the secret and the expiration data points needed by the
JwtService class.
Go ahead and run your app right now. If Microsoft hasn't changed the template by the time you are following this article, you should probably get some fake weather json data on your browser.
This means the app is working and currenlty not requiring any kind of authentication to serve up data. Let's go ahead and mess that up! :)
Go head add the [Authorize] attribute, you will need to bring in the Microsoft.AspNetCore.Authorization and try running the project again.
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.Extensions.Logging;
-
- namespace AuthTest.API.Controllers
- {
- [ApiController]
- [Authorize]
- [Route("[controller]")]
- public class WeatherForecastController : ControllerBase
- {
- private static readonly string[] Summaries = new[]
- {
- "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
- };
-
- private readonly ILogger<WeatherForecastController> _logger;
-
- public WeatherForecastController(ILogger<WeatherForecastController> logger)
- {
- _logger = logger;
- }
-
- [HttpGet]
- public IEnumerable<WeatherForecast> 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();
- }
- }
- }
When I ran the project I got the error below. The error means that ASP.NET Core sees the [Authorize] attribute but doesnt know how to handle that, because we haven't configured a middleware to do so. Let's go ahead a take care of that.
Back to the project go ahead and create a new class inside the Middleware folder, let's call this one AuthenticationMiddleware
Before we make any changes to this new class we need to bring one more Nuget package:
- Microsoft.AspNetCore.Authentication.JwtBearer
Browse and install the above package, and update the AuthenticationMiddleware class with the code below
- using System.Text;
- using Microsoft.IdentityModel.Tokens;
- using Microsoft.Extensions.Configuration;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.AspNetCore.Authentication.JwtBearer;
-
- namespace AuthTest.API.Middleware
- {
- public static class AuthenticationExtension
- {
- public static IServiceCollection AddTokenAuthentication(this IServiceCollection services, IConfiguration config)
- {
- var secret = config.GetSection("JwtConfig").GetSection("secret").Value;
-
- var key = Encoding.ASCII.GetBytes(secret);
- services.AddAuthentication(x =>
- {
- x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
- x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
- })
- .AddJwtBearer(x =>
- {
- x.TokenValidationParameters = new TokenValidationParameters
- {
- IssuerSigningKey = new SymmetricSecurityKey(key),
- ValidateIssuer = true,
- ValidateAudience = true,
- ValidIssuer = "localhost",
- ValidAudience = "localhost"
- };
- });
-
- return services;
- }
- }
- }
Now that we have the middleware built we need to hook it up to our services.
Open the Startup.cs class, find the ConfigurationServices, and the Configure functions and update with the code below.
You will have to import the reference to the namespace where the AuthenticationExtension is located
- using AuthTest.API.Middleware;
- using Microsoft.AspNetCore.Builder;
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.Extensions.Configuration;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Hosting;
-
- namespace AuthTest.API
- {
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
-
- public IConfiguration Configuration { get; }
-
-
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddControllers();
- services.AddTokenAuthentication(Configuration);
- }
-
-
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
-
- app.UseHttpsRedirection();
-
- app.UseRouting();
- app.UseAuthentication();
- app.UseAuthorization();
-
- app.UseEndpoints(endpoints =>
- {
- endpoints.MapControllers();
- });
- }
- }
- }
Cool! Let's go ahead a run a quick test! Go ahead and just start your app again.
Did you get a "This page isn't working" - If you did, good job! That's exactlyy what we are lookin for. No more exceptions!
If you look closely you will see the server is displaying a 401 error - which means... drum roll... Unauthorized: https://httpstatuses.com/401
Our app now understands what we are looking for and since it didnt see a token in the request it returned an 401 - Unathorized error.
Stop the app and let's go ahead and create a new controller. Name this one TokenController
Right-click the Controllers folder and choose Add -> Controller...
Pick the API Controller - Empty template and click Add
- using AuthTest.API.Services;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.Extensions.Configuration;
-
- namespace AuthTest.API.Controllers
- {
-
- [Route("api/[controller]")]
- [ApiController]
- public class TokenController : ControllerBase
- {
- private IConfiguration _config;
-
- public TokenController(IConfiguration config)
- {
- _config = config;
- }
-
- [HttpGet]
- public string GetRandomToken()
- {
- var jwt = new JwtService(_config);
- var token = jwt.GenerateSecurityToken("[email protected]");
- return token;
- }
- }
- }
Restart the app, and navigate to https://localhost:44363/api/token, your port number may vary.
Hopefully you got a token like me,
Now with Postman or Fiddler whichever tool you prefer, let's try to call into the WeatherForecastController and see if we can get through.
With the app running let's go ahead and make a call into the token endpoint to get a fresh token and then let's use that token to call into the weather forecast service.
Make sure your app is running end do a GET on .../api/token
This is what fiddler looks like.
On the Composer tab choose GET from the dropdown and type in your URL, then click the Execute button.
Double click the result on the left and then click on decode, to see your actual token.
Important: This is only happening because I am running my app in HTTPS. If I was running in HTTP, I would not need to decode the result.
After decoding the yellow text goes away and you can copy the token:
Let's compose another call into the WeatherForecast endpoint.
GET
header area:
User-Agent: Fiddler
Host: localhost:44363
Authorization: Bearer YOUR TOKEN GOES HERE
Click the Execute button:
There it is - data!
In the next article I will cover actually creating a login control with Create Account and Log in so we can further learn about security.
Thank you for reading and comment below if you ran into any issues, or if you have suggestions.
Thank you again.