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>