Saturday, April 13, 2019

JWT on .NET Core 2.2 and Little Else

I wanted to see how to implement JSON Web Tokens (JWT) in C#/ASP.NET Core 2.2 without the chaos of anything else, like passwords, a database or anything else. I have a complete example project on my GitHub. If you clone my respository, you can skip downn to Discussion.

Solution

I created a project that I called "SimpleJwt4Core22" that was configured not to use SSL and use port 55477 (so the URL would be "http://localhost:55477")

  1. Add the following configuration to appsettings.json:

    "Jwt": {
      "Site": "http://jrcs3.com",
      "SigningKey": "Not a safe place to put a security key, eh?",
      "ExperyInMinutes": "600"
     },
    
  2. Ensure that IConfiguration is injected into Startup.cs, the constructor and private field should look like this:

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    
    public IConfiguration Configuration { get; }
    
    (depending on which template you used to create the project, this code may already be present.)
  3. Configure site to use JWT for its AuthenticationScheme in Startup’s ConfigureServices() method.

    // JWT Begin Insert
    services.AddAuthentication(option =>
    {
      option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
      option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
      option.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(options =>
    {
      options.SaveToken = true;
      options.RequireHttpsMetadata = true;
      options.TokenValidationParameters = new TokenValidationParameters()
      {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidAudience = Configuration["Jwt:Site"],
        ValidIssuer = Configuration["Jwt:Site"],
        IssuerSigningKey = new SymmetricSecurityKey(
          Encoding.UTF8.GetBytes(Configuration["Jwt:SigningKey"]))
      };
    });
    // JWT End Insert
    
  4. Ensure that MVC is configured at the end of Startup’s ConfigureServices() method.

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    

    (depending on which template you used to create the project, this code may already be present.)

  5. Add the following line to Startup’s Configure() method (just below the IsDevelopment() block)

    // JWT Begin Insert
    app.UseAuthentication();
    // JWT End Insert
    
  6. Ensure that MVC is enabled in Startup’s Configure() method, you should see this line:

    app.UseMvc();
    
    (depending on which template you used to create the project, this code may already be present.)
  7. Create a Controller and a ViewModel. I called the Controller "JwtController" and put it in the Controllers directory (create it if you need to) and I called ViewModel "MakeTokenViewModel"

    So here’s the code for the /ViewModels/MakeTokenViewModel.cs:

    using System.ComponentModel.DataAnnotations;
    
    namespace SimpleJwt4Core22.ViewModels
    {
      public class MakeTokenViewModel
      {
        [Required]
        public int Id { get; set; }
        [Required]
        public string UserName { get; set; }
        [Required]
        public string Role { get; set; }
        // To simulate login or other failure:
        public bool Fail { get; set; }
      }
    }
    

    And /Controllers/JwtController.cs:


    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Configuration;
    using Microsoft.IdentityModel.Tokens;
    using SimpleJwt4Core22.ViewModels;
    using System;
    using System.IdentityModel.Tokens.Jwt;
    using System.Security.Claims;
    using System.Text;
    
    namespace SimpleJwt4Core22.Controllers
    {
      [Route("api/[controller]")]
      [ApiController]
      public class JwtController : ControllerBase
      {
        private readonly IConfiguration _configuration;
        public JwtController(IConfiguration configuration)
        {
          _configuration = configuration;
        }
    
        [Route("maketoken")]
        [HttpPost]
        public ActionResult Login([FromBody] MakeTokenViewModel model)
        {
          // Simulate login or other failure:
          if (model.Fail)
          {
            return BadRequest("JWT Creation Failure");
          }
    
          var claim = new[]
          {
            new Claim("name", model.UserName),
            new Claim("id", model.Id.ToString()),
            new Claim("role", model.Role)
          };
          var signingKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_configuration["Jwt:SigningKey"]));
          int experyInMinutes = Convert.ToInt32(_configuration["Jwt:ExperyInMinutes"]);
    
          var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Site"],
            audience: _configuration["Jwt:Site"],
            expires: DateTime.UtcNow.AddMinutes(experyInMinutes),
            signingCredentials: new SigningCredentials(
              signingKey, SecurityAlgorithms.HmacSha256),
            claims: claim
          );
          return Ok(new JwtSecurityTokenHandler().WriteToken(token));
        }
      }
    }
    

    At his point you should be able to POST to the endpoint http://localhost:55477/api/jwt/maketoken with Postman, Fiddler or the following the curl command:

    curl -X POST -H "Content-Type: application/json" \
      -d '{"UserName": "dvader","Role": "admin","Id": 42}' \
      http://localhost:55477/api/jwt/maketoken
    

    I like to give curl command because they are short and sweet. On Windows 10 you can run curl on Windows Subsystem for Linux or Git Bash or a number of other shells. I added the line continuation character ('\') for readability. (In a future post I will consume this endpoint in Javascript

  8. To use the JWT, we need some endpoint that uses it. I created /Controllers/ValuesController.cs, with endpoints that provides with different endpoints that require different roles (admin & super):


    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using Newtonsoft.Json;
    using System.Collections.Generic;
    using System.Security.Claims;
    
    namespace SimpleJwt4Core22.Controllers
    {
      [Authorize]
      [Route("api/[controller]")]
      [ApiController]
      public class ValuesController : ControllerBase
      {
        private readonly JsonSerializerSettings _serializerSettings;
        public ValuesController()
        {
          _serializerSettings = new JsonSerializerSettings
          {
            Formatting = Formatting.Indented
          };
        }
        // GET api/values
        [HttpGet]
        public IActionResult Get()
        {
          return handleRequest();
        }
    
        // All of these endpoints do the same thing except for the 
        // authorized roles
        [Route("admin")]
        [Authorize(Roles = "admin")]
        [HttpGet]
        public IActionResult GetAdmin()
        {
          return handleRequest();
        }
    
        [Route("super")]
        [Authorize(Roles = "super")]
        [HttpGet]
        public IActionResult GetSuper()
        {
          return handleRequest();
        }
    
        [Route("either")]
        [Authorize(Roles = "super,admin")]
        [HttpGet]
        public IActionResult GetBoth()
        {
          return handleRequest();
        }
    
        [Route("open")]
        [AllowAnonymous]
        [HttpGet]
        public IActionResult GetOpen()
        {
          return handleRequest();
        }
    
        private IActionResult handleRequest()
        {
          // Read the claims that I wrote in JwtController:
          var claims = ((ClaimsIdentity)User.Identity).Claims;
          var id = getClaimByType(claims, "id");
          var name = getClaimByType(claims, "name");
          // In JwtController, I created a claim for "role" 
          // so I would expect this to have a value:
          var role = getClaimByType(claims, "role");
          // however, by magic, this one has the value:
          var msRole = getClaimByType(claims, 
              "http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
          var response = new
          {
            id,
            name,
            role,
            msRole
          };
          // Send the claims back in the Response:
          var json = JsonConvert.SerializeObject(response, _serializerSettings);
          return new OkObjectResult(json);
        }
    
        public static string getClaimByType(IEnumerable<Claim> jwt, string typeKey)
        {
          foreach (var claim in jwt)
          {
            if (claim.Type == typeKey)
            {
              return claim.Value;
            }
          }
          return string.Empty;
        }
      }
    }
    

    Again we are ready to make a GET to the endpoint http://localhost:55477/api/values (or one if it's sub routes) with Postman, Fiddler or the following the curl command (this time we need to send the JWT:

    jwt=$( \
      curl -X POST -H  "Content-Type: application/json" \
        -d '{"UserName": "ckent","Role": "super","Id": 42,"fail":false }' \
        http://localhost:55477/api/jwt/maketoken \
    )
    curl -X GET -H "Authorization: Bearer $jwt" http://localhost:55477/api/values/super
    


Discussion

The code in Startup.cs configures and enables Authentication and sets it to the JwtBearer scheme. I put some configuration data in appsettings.json.

Yes, this is a rather stilted example. It does not validate a password and allows the user to "log in" with any role. I have one endpoint to obtain a JWT, and several others to simulate getting data with different required roles (and one that doesn’t even require you to be logged in at all).

Logging In/Getting your JWT

To make a JWT you need to create an IEnumerable of Claims that you wish to make:

var claim = new[]
{
    new Claim("name", model.UserName),
    new Claim("id", model.Id.ToString()),
    new Claim("role", model.Role)
};

Then you construct a new JwtSecurityToken with the claims, signing key, expery time, etc to make your JWT:

var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
    _configuration["Jwt:SigningKey"]));
int experyInMinutes = Convert.ToInt32(_configuration["Jwt:ExperyInMinutes"]);
string site = _configuration["Jwt:Site"];

var token = new JwtSecurityToken(
    issuer: site,
    audience: site,
    expires: DateTime.UtcNow.AddMinutes(experyInMinutes),
    signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
    claims: claim
);

Return the JWT. Refer to JwtController.Login() for the complete example.

Securing your endpoints

The endpoints in ValuesController are secured with attributes. I applied the AuthorizeAttribute to the class, so you must be logged in to hit any endpoint in the class, unless the AllowAnonymousAttribute has been applied to the endpoint's method. You can further restrict access to an endpoint by applying an AuthorizeAttribute Roles set.

Here are all the endpoints:

Endpoint (URL) Attribute Notes
/api/values Must be logged in (have a JWT) with ANY role
/api/values/admin [Authorize(Roles = "admin")] Must be logged with the role "admin"
/api/values/super [Authorize(Roles = "super")] Must be logged with the role "super"
/api/values/either [Authorize(Roles = "admin,super")] Must be logged with either role "admin" or "super"
/api/values/open [AllowAnonymous] DOES NOT need to be logged in (doesn't need JWT)