.NET Core JWT Authentication
In today's article, I will show you an easy implementation of JWT Bearer Tokens in .NET Core. To keep things simple, I will not be connecting to any persistent storage for user management, but you are welcome to expand on this example.
So, what are JWT Bearer Tokens? According to the official description, "JWT (JSON Web Token) is an open standard ( RFC 7519 ) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object." This is a fancy way of saying that by using bearer tokens, you can secure your applications using a simple JSON object in a nice, compact manner which is great for web applications.
But you are here to see some code, right? Let's dive in...
We start off by creating a new .NET Core Web API application:
Once our project is created, we start by configuring the Startup.cs. The purpose of this step is to tell our application that we want to use JWT for authentication. Additionally, we also need to tell the application what to look for when dealing with JWT.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddControllers();
}
In the code above, we are instructing our application that we want to add authentication in the form of JWT Bearer Tokens, and we are providing some information which specifies what should be validated during token validation:- ValidateIssuer = true, instructs the application that we want to validate the Issuer of the token
- ValidateAudience = true, instructs the application that we want to validate the Audience of the token
- ValidateLifetime = true, instructs the application that we want to validate the lifetime of the token
- ValidateIssuerSigningKey = true, instructs the application that we want to validate the Security Key that signed the token.
- ValidIssuer is a string which represents a valid issuer for the token
- ValidaAudience is a string which represents a valid audience that will be used to check against the token's audience
- IssuerSigningKey is a security key which is used for signature validation
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Since we told the application that it should retrieve some values from the application's configuration file, we need to add those entries as follows:
// JWT
"Jwt:Key": "fc041164-2dc9-45ff-8f66-45217a8e7694",
"Jwt:Issuer": "localhost"
At this point, our application will be able to start up, but it will not do anything in terms of authentication as we have not secured any of our controllers or actions and we have not implemented any code to handle login attempts by a user.
To keep things simple, I have created a Login controller as shown below
using JWTBearerTokens.RequestModels.Login;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace JWTBearerTokens.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
private IConfiguration _configuration;
public LoginController(IConfiguration configuration)
{
_configuration = configuration;
}
[AllowAnonymous]
[HttpPost]
public IActionResult Login([FromBody] UserModel requestModel)
{
IActionResult response = Unauthorized();
var user = AuthenticateUser(requestModel);
if (user != null)
{
var tokenString = GenerateJSONWebToken(user);
response = Ok(new { token = tokenString });
}
return response;
}
#region PRIVATE
private UserModel AuthenticateUser(UserModel requestedLogin)
{
// This is just a demo. No database for users
UserModel result = null;
if (requestedLogin.Username.ToLower() == "admin@admin.com")
result = new UserModel { Username = "admin@admin.com", Email = "admin@admin.com" };
return result;
}
private string GenerateJSONWebToken(UserModel user)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Username),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(ClaimsIdentity.DefaultRoleClaimType, "admin"),
new Claim(ClaimsIdentity.DefaultRoleClaimType, "user"),
new Claim(ClaimsIdentity.DefaultRoleClaimType, "manager"),
new Claim(ClaimsIdentity.DefaultRoleClaimType, "weatherman")
};
var token = new JwtSecurityToken(_configuration["Jwt:Issuer"],
_configuration["Jwt:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(120),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
#endregion
}
}
Our UserModel looks like this:
namespace JWTBearerTokens.RequestModels.Login
{
public class UserModel
{
public string Username { get; set; }
public string Password { get; set; }
public string Email { get; set; }
}
}
From the controller code, you can see that we allow anonymous connections to the Login method of the controller. This allows unauthenticated users to attempt a login. In a production environment, the "AuthenticateUser" method will be replaced with something a bit more robust, which will connect to some form or permanent storage.
Once a user is authenticated, we generate a token to be sent back. In our example, we also include some hardcoded roles, but again, in a production environment, this will come from storage.
There is a debate on whether or not to include roles as part of the token and there are pros and cons for each side of the argument, but that falls outside the scope of this article.
Now that we have a way of authenticating users and generating tokens, we can give it a spin to see whether we can correctly generate a token. Using Postman, we can submit a request similar to the following, depending on your port numbers, etc.
If we did our job correctly, the request should return a bearer token in the form of an encrypted string. Once we have the string, we can secure our controllers. As part of our example, we will attempt to secure the default WeatherForecastController.
Firstly we want to secure this entire controller. This can be achieved by adding the [Authorize] attribute at the controller level. Secondly, we only want users who have the role of "weatherman" to be able to access the Get() method. This is achieved by specifying the Roles property on the [Authorize] attribute on the Get action, as shown below:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
namespace JWTBearerTokens.Controllers
{
[ApiController]
[Route("[controller]")]
[Authorize]
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]
[Authorize(Roles = "weatherman")]
public IEnumerable<weatherforecast> Get()
{
var currentUser = HttpContext.User;
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();
}
}
}
Using Postman, we can now attempt to call the Get() method on the WeatherForecast controller.
As you might notice, we received a 401 - Unauthorized error. You will receive this error if you have not set the Authorization for the request in Postman. For the request to be authenticated, we need to use the token we generated with the previous (Login) request:
Now that we have set the authentication, we can try the request again. This time, we should receive a proper response:
That is the simplest way of implementing JWT and I encourage you to play around with the various options available for implementing JWT.
Happy coding!