Friday, May 03, 2019

Vanilla JavaScript for the Client Side of my JWT Server App

This is the Vanilla JavaScript client side code to accompany JWT on .NET Core 2.2 and Little Else. I have the sourcecode on my Github account as a branch of the other project.

Logging in (as far as it goes)

To log in, I need to make a request to my services with the data gathered from my “login” form. The request is a POST and the in sent to the server as JSON (“content-Type:application/json” in the head. When I get a response back from the server, XMLHttpRequest calls getJwtProcessResponse():

function getJwt() {
  showResults("working ....");

  // Assemble data to log in, should match schema of MakeTokenViewModel
  const userName = document.getElementById("txtUserName").value;
  const role = document.getElementById("txtRole").value;
  const id = document.getElementById("txtId").value;
  const fail = document.getElementById("chkFail").checked;
  const message = 
    `{"UserName":"${userName}","Role": "${role}","Id": ${id},"Fail":${fail}}`;

  // Make the request
  const xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = getJwtProcessResponse;
  xhttp.open("Post", `${BASE_URL}jwt/maketoken`, true);
  xhttp.setRequestHeader("content-Type", "application/json");
  xhttp.send(message);
}

When I get a response back and it’s ready: if the status is 200, login succeeded, and I store the JWT in Local Storage with writeJwt() (since I’m not sending anything other than the JWT, I don’t have to parse it out of this.responseText). Otherwise, alert the user of the failure.

// Get and process the request
function getJwtProcessResponse() {
  if (this.readyState == 4) {
    if (this.status == 200) {
      writeJwt(this.responseText);
      showResults("token written to localStorage");
    }
    else {
      alert(`${this.status}\n ${this.responseText}`);
        showResults("Failed!");
    }
  }
}

Making a call to the Service

Here I am going to make a simple GET request with the JWT in Local Storage (if available)

// Make a call to the end point specified by urlExtension
// (each endpoint has a different permission, for demo purpose)
function makeCall(urlExtension) {
  showResults("working ....");
  const jwt = readJwt();
  const xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = makeCallProcessResponse;
  xhttp.open("Get", `${BASE_URL}values${urlExtension}`, true);
  // We want to test with the user not logged in
  if (!!jwt) {
      xhttp.setRequestHeader("Authorization", ` Bearer  ${jwt}`);
  }
  xhttp.send();
}

When I get a response back and it’s ready: if the status is 200, I am authorized to and call showResults() to show what I got back. Otherwise the user is alerted about the failure

function makeCallProcessResponse() {
  if (this.readyState == 4) {
    if (this.status == 200) {
      showResults(this.responseText);
    }
    else {
      alert(`${this.status}\n ${this.responseText}`);
      showResults("Failed!");
    }
  }
}

Logging out

You really don’t log off JWT, they expire. To simulate logging out of a site, you make Local Storage forget:

function deleteJwt() {
  clearJwt();
}

Reading JWT “payload”

You can embed data in middle part of the JWT (the Payload) as unencrypted Base64 encoded JSON (Yes, I stole it from StackOverflow).

//see https://stackoverflow.com/a/38552302/3819
function parseJwt(token) {
  const base64Url = token.split('.')[1];
  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
  return JSON.parse(window.atob(base64));
}

The localStorage functions

These are the functions that actually read and write the JWT to localStorage:

function writeJwt(jwt) {
  if (typeof Storage !== "undefined") {
    localStorage.setItem("jwt", jwt);
  } else {
    showResults("Sorry, your browser does not support Web Storage...")
  }
}

function readJwt() {
  if (typeof Storage !== "undefined") {
    return localStorage.getItem("jwt");
  } else {
    showResults("Sorry, your browser does not support Web Storage...");
  }
  return "";
}

function clearJwt() {
  if (typeof Storage !== "undefined") {
    localStorage.removeItem("jwt");
  } else {
    showResults("Sorry, your browser does not support Web Storage...");
  }
}

The Whole Page

And here is the whole page (JS, CSS and HTML all in one):

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 25px;
    }

    label {
      width: 80px;
      display: inline-block;
      margin: 2px;
    }

    input {
      margin: 2px;
    }

    button {
      width: 150px;
      margin: 2px;
    }

    #result {
      width: 100%;
      height: 150px;
    }
    .grid-container {
      width: 100%;
      display: grid;
      grid-gap: 10px;
      grid-template-columns: 1fr 1fr 1fr;
    }

    .grid-container {
      display: inline-grid;
    }
  </style>
</head>
<body>
  <h2>JWT Client</h2>
  <div id="theGrid"class="grid-container">
    <div id="data"class="grid-item" >
      <h3>Data for JWT</h3>
      <p>This is the data used to create the JWT. The Server recognizes 2 roles: "admin" and "super".</p>
      <p>(We can simulate a log in failure by checking "Fail")</p>
      <label for="txtUserName">User Name </label><input type="text" id="txtUserName" value="johns" /><br />
      <label for="txtRole">Role</label><input type="text" id="txtRole" value="admin" /><br />
      <label for="txtId">Id</label><input type="number" id="txtId" value="42" /><br />
      <label for="chkFail">Fail</label><input type="checkbox" id="chkFail" value="false" /><br />
    </div>
    <div id="access" class="grid-item">
      <h3>Make JWT</h3>
      <p>Here we get the JWT from the "server", store and retrieve the JWT.</p>
      <p>The JWT is stored in Local Storage.</p>
      <button type="button" onclick="getJwt()">Get Token</button><br />
      <button type="button" onclick="showJwt()">Show Token</button><br />
      <button type="button" onclick="decode()">Decode</button><br />
      <button type="button" onclick="deleteJwt()">Clear Token</button><br />
    </div>
    <div id="use" class="grid-item">
      <h3>Use JWT</h3>
      <p>Here we use the JWT we stored in Local Storage and make calls to       
      various endpoints which have different permissions on the server</p>
      <p>The Server decodes the claims in the JWT and returns them as JSON object</p>
      <button type="button" onclick="makeCall('')">Call "/"</button><br />
      <button type="button" onclick="makeCall('/admin')">Call "/admin"</button><br />
      <button type="button" onclick="makeCall('/super')">Call "/super"</button><br />
      <button type="button" onclick="makeCall('/either')">Call "/either"</button><br />
      <button type="button" onclick="makeCall('/open')">Call "/open"</button><br />
    </div>
  </div>
  <div id="results">
    <label for="result">Results</label><br />
    <textarea id="result"></textarea>
  </div>
  <script>
    const BASE_URL = "/api/";
    function getJwt() {
      showResults("working ....");

      // Assemble data to log in, should match schema of MakeTokenViewModel
      const userName = document.getElementById("txtUserName").value;
      const role = document.getElementById("txtRole").value;
      const id = document.getElementById("txtId").value;
      const fail = document.getElementById("chkFail").checked;
      const message = `{"UserName":"${userName}","Role": "${role}","Id": ${id},"Fail":${fail}}`;

      // Make the request
      const xhttp = new XMLHttpRequest();
      xhttp.onreadystatechange = getJwtProcessResponse;
      xhttp.open("Post", `${BASE_URL}jwt/maketoken`, true);
      xhttp.setRequestHeader("content-Type", "application/json");
      xhttp.send(message);
    }

    // Get and process the request
    function getJwtProcessResponse() {
      if (this.readyState == 4) {
        if (this.status == 200) {
          writeJwt(this.responseText);
          showResults("token written to localStorage");
        }
        else {
          alert(`${this.status}\n ${this.responseText}`);
          showResults("Failed!");
        }
      }
    }

    // Make a call to the end point specified by urlExtension
    // (each endpoint has a different permission, for demo purpose)
    function makeCall(urlExtension) {
      showResults("working ....");
      const jwt = readJwt();
      const xhttp = new XMLHttpRequest();
      xhttp.onreadystatechange = makeCallProcessResponse;
      xhttp.open("Get", `${BASE_URL}values${urlExtension}`, true);
      // We want to test with the user not logged in
      if (!!jwt) {
        xhttp.setRequestHeader("Authorization", ` Bearer  ${jwt}`);
      }
      xhttp.send();
    }

    function makeCallProcessResponse() {
      if (this.readyState == 4) {
        if (this.status == 200) {
          showResults(this.responseText);
        }
        else {
          alert(`${this.status}\n ${this.responseText}`);
          showResults("Failed!");
        }
      }
    }

    function showJwt() {
      const jwt = readJwt();
      if (jwt === null) {
        showResults("no token to display");
        return;
      }
      showResults(jwt);
    }

    function deleteJwt() {
      clearJwt();
    }

    function writeJwt(jwt) {
      if (typeof Storage !== "undefined") {
        localStorage.setItem("jwt", jwt);
      } else {
        showResults("Sorry, your browser does not support Web Storage...")
      }
    }

    function readJwt() {
      if (typeof Storage !== "undefined") {
        return localStorage.getItem("jwt");
      } else {
        showResults("Sorry, your browser does not support Web Storage...");
      }
      return "";
    }

    function clearJwt() {
      if (typeof Storage !== "undefined") {
        localStorage.removeItem("jwt");
      } else {
        showResults("Sorry, your browser does not support Web Storage...");
      }
    }

    function decode() {
      const jwt = readJwt();
      if (jwt == null) {
        showResults("no token to decode");
        return;
      }
      const parsed = parseJwt(jwt);
      showResults(JSON.stringify(parsed, null, 2));
    }

    //see https://stackoverflow.com/a/38552302/3819
    function parseJwt(token) {
      const base64Url = token.split('.')[1];
      const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
      return JSON.parse(window.atob(base64));
    };

    function showResults(results) {
      document.getElementById("result").value = results;
    }
  </script>
</body>
</html>

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)