JavaScript Web Applications and JSON Web Token (JWT) Security

Background

If you do not want to build and pay for infrastructure to cache Session State so it can be shared across multiple Web servers, then security between the browser and Web Application back-end can be implemented in a stateless manner with JSON Web Tokens (JWT).

Ideally, a JWT is only ever briefly stored in browser memory in a private JavaScript field. In an alternative scenario where a JavaScript Web Application does not have a back-end and is performing API calls to various services or an API Gateway, storing the JWT in memory may be fine, possibly due to that API requiring a one-time Access Token that must be retrieved just before each API call.

However for a Web Application with a back-end, where the front-end only communicates to its back-end and does not perform API calls to other services, if the JWT is stored in memory, a simple browser page refresh will wipe the JWT from memory and the user will be forced to sign-in to the front-end again.

In order to store the JWT Access Token so that it persists between page refreshes, good security practices need to be followed in order to avoid and mitigate the possible attack vectors.

Methodology

The safest methodology is suggested by OWASP - Token Storage on Client Side - see my explanation below.

1. Back-end: Sign-in

On the back-end, after a user’s credentials have been validated successfully:

  1. Generate a cryptographically strong random string - we will name this the fingerprint
  2. Perform a SHA256 hash of the fingerprint
  3. Create the JWT Access Token with the desired expiration time, and a Claim for the fingerprint, with the value of the previously calculated hash.
  4. Add a hardened cookie to the HTTP Response for the fingerprint, with a value of the full random string.
  5. Return the JWT in the HTTP Response Header or Body, depending on how you prefer. DO NOT return the JWT in a Cookie!

NOTE: a hardened cookie specifies:

  • Secure - so that the cookie is only ever transmitted via HTTPS - to help prevent man-in-the-middle attacks;
  • HttpOnly - so that the cookie cannot be retrieved via JavaScript - to help mitigate cross-site scripting (XSS) attacks;
  • SameSite=strict - so that the cookie is sent only ever to the same site as the one that originated it; and
  • '__Host-’ as the prefix to the Cookie name without a Domain attribute and specifying a Path attribute of / - so that the back-end server can confirm that the cookie was originally set on a secure origin (not from a browser) and help mitigate a session fixation attack.

2. Browser: Token Storage

The Web browser will receive both the JWT Access Token and fingerprint hardened cookie.

There is nothing for us to do with the fingerprint hardened cookie. Since this cookie is hardened, JavaScript cannot be used to retrieve it, thus mitigating XSS attacks on it.

Store the JWT Access Token in the browser Session Storage. This will be cleared when the browser Tab or Window is closed.

Since the JWT is stored in Session Storage, it possibly could be stolen by an XSS attack - however we mitigate this risk since the fingerprint Claim in the JWT allows our back-end to prevent reuse of a stolen JWT by an attacker on their machine.

3. Browser: Perform a Back-end Call

When a call is to made to the back-end:

  1. the browser will automatically send the fingerprint cookie as part of the HTTP Request; and
  2. you need to retrieve the JWT from Session Storage and add it to the HTTP Request header or body, depending on your implementation.

4. Back-end: Authorise Web Request

Now completing the picture, when the back-end receives a HTTP Request over HTTPS:

  1. Validate that the JWT was signed by our system;
  2. Validate that the JWT has not expired;
  3. Perform a SHA256 hash on the value of the fingerprint hardened cookie; and
  4. Assert that the hash we just computed is the same as the value in the fingerprint Claim in the JWT.

Conclusion

With all these measures in place, we can be confident that the HTTP Request legitimately came from our front-end.

NOTE: don’t forget also to add a Content Security Policy to restrict the domains from which your web resources may be retrieved, and apply any other relevant defense in depth practices.

ASP.NET MVC2 AJAX: Executing Dynamically Loaded JavaScript

(or ASP.NET MVC2 AJAX: ASP.NET MVC2 Client Validation in an AJAX Loaded Partial View)

I have been tinkering with ASP.NET MVC2 for a while and I had the problem where the MVC2 Client Validation did not work when I was dynamically loading a partial view through MVC2 AJAX. Upon further investigation, I discovered that the issue was not limited just to MVC2 Client Validation, but to all JavaScript that is dynamically loaded through MVC2 AJAX. The issue has to do with the way that the response is injected into the DOM element - through the InnerHTML property. Any script block injected into that property will not be executed.

Note: when I refer to the ASP.NET MVC2 AJAX, I am specifically referring to the usage of method calls such as Ajax.ActionLink() and Ajax.BeginForm().

There are many blogs and tutorials that discuss how to load partial views asynchronously with AJAX in ASP.NET MVC2, and there are even more articles on how do that with jQuery. Unfortunately most of these deal with the simplistic scenario of explaining the basics of partial views.

It is difficult to find any information on how to do MVC2 Client Validation (or use any JavaScript) from within a partial view that is loaded dynamically with ASP.NET MVC2 AJAX. There are however a number of forum posts with people questioning exactly how to do this!

Now, I am not going to discuss how it might be better practice to put all the JavaScript in separate .js files and load those when the page is initially loaded. While I agree, there are times when that is simply not an option… MVC2 Client Validation is one such example, as it emits JavaScript when the partial view is rendered.

Side note: I originally came across this same type of behaviour is ASP.NET Web Forms in 2007, with the AjaxControlToolkit UpdatePanel. A workable solution for ASP.NET Web Forms is described here: http://weblogs.asp.net/infinitiesloop/archive/2007/09/17/inline-script-inside-an-asp-net-ajax-updatepanel.aspx.

Below are some links to various posts by other people who have had similar issues.

In summary, the responses essentially say three things:

  1. ASP.NET MVC AJAX will not execute JavaScript that is dynamically loaded into a DOM element;

  2. The only workable solution is to not use MVC AJAX and instead write JavaScript and use jQuery to do all the AJAX; and

  3. ASP.NET MVC2 Client Validation needs to be reinitialised after a partial view is dynamically loaded and the JavaScript is executed.

I found these responses to be unsatisfactory! Are people seriously suggesting that one does all of that in the AjaxOption.OnSuccess delegate every time? Right now, I want to take advantage of MVC2 Client Validation and I want to use MVC AJAX - because it is simpler than the alternatives. It should ‘just work’, shouldn’t it? Am I asking too much? After all, this is the second version of ASP.NET MVC!

So noting that JavaScript is a dynamic language, I created the following solution which seamlessly extends the ASP.NET MVC2 JavaScript to make the execution of dynamically loaded JavaScript and the reinitialisation of MVC2 Client Validation happen automatically.

Without diving deeply into the code, the approach is to override the MicrosoftMvcAjax.js Sys.Mvc.MvcHelpers._onComplete method so that I can then hook into the ajaxOptions.onSuccess delegate in order to use jQuery.globalEval() to execute the JavaScript dynamically loaded into the target element, and then finally reinitialise the MVC2 Client Validation.

Just by including this script in an application (carefully loaded AFTER the jQuery and MicrosoftMVCValidation scripts!), this will now happen seamlessly on every MVC2 AJAX request.

Yay! Now the dynamically loaded JavaScript executes, and the MVC2 Client Validation works on partial views that are loaded dynamically with MVC AJAX!

AjaxLoadedContentScriptFix.js

  1////////////////////////////////////////////////////////////////////////////////
  2//
  3// Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix
  4//
  5// Original Author: Adam M Craven (https://devblog.channeladam.com)
  6//
  7// This extension to ASP.NET MVC2 makes any script loaded as part of dynamically loaded content
  8// be executable and reinitialises the MVC2 client validation to enable that processing to occur
  9// for an AJAX loaded view.
 10//
 11// Warning: This script may not work with future versions of the minimised MicrosoftMvcAjax.js.
 12//
 13// When content (e.g. Partial Views) is dynamically loaded into a DOM target element through AJAX
 14// via the standard ASP.NET MVC2 "Ajax.ActionLink()" or "Ajax.BeginForm()" methods, any script
 15// (such as javascript or MVC client validation script) that was emitted is not executed because
 16// the content is assigned to the target element's "innerHTML" property. This will not cause the
 17// script to be executed or execuatable.
 18//
 19// Must be included after jQuery and the MicrosoftMvcValidation javascript, typically like this:
 20//    <script type="text/javascript" src="<%=Url.Content("~/Scripts/jquery-1.4.1.min.js") %>"></script>
 21//    <script type="text/javascript" src="<%=Url.Content("~/Scripts/MicrosoftAjax.js") %>"></script>
 22//    <script type="text/javascript" src="<%=Url.Content("~/Scripts/MicrosoftMvcAjax.js") %>"></script>
 23//    <script type="text/javascript" src="<%=Url.Content("~/Scripts/MicrosoftMvcValidation.js") %>"></script>
 24//    <script type="text/javascript" src="<%=Url.Content("~/Scripts/AjaxLoadedContentScriptFix.js") %>"></script>
 25//
 26
 27Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix = function Sys_Mvc_MvcHelpers_AjaxLoadedContentScriptFix() {}
 28
 29Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._onComplete = function Sys_Mvc_MvcHelpers_AjaxLoadedContentScriptFix$_onComplete(request, ajaxOptions, ajaxContext) {
 30    /// <param name="request" type="Sys.Net.WebRequest">
 31    /// </param>
 32    /// <param name="ajaxOptions" type="Sys.Mvc.AjaxOptions">
 33    /// </param>
 34    /// <param name="ajaxContext" type="Sys.Mvc.AjaxContext">
 35    /// </param>
 36
 37    // Hook into the ajaxOptions.onSuccess delegate
 38    ajaxContext._ajaxLoadedContentScriptFixOrigAjaxOptionsOnSuccess = ajaxOptions.onSuccess;
 39    ajaxOptions.onSuccess = Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._ajaxOptionsOnSuccess;
 40
 41    // Call the original MVC onComplete method
 42    Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._origOnComplete(request, ajaxOptions, ajaxContext);
 43}
 44
 45Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._ajaxOptionsOnSuccess = function Sys_Mvc_MvcHelpers_AjaxLoadedContentScriptFix$_onSuccess(ajaxContext) {
 46    /// <param name="ajaxContext" type="Sys.Mvc.AjaxContext">
 47    /// </param>
 48
 49    // Make any dynamically loaded script execute
 50    Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._globalEvalScriptInElementId(ajaxContext.get_updateTarget());
 51
 52    // Reinitialise the MVC validation
 53    Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._reinitialiseMvcValidation();
 54
 55    // Call the original success delegate
 56    if (ajaxContext._ajaxLoadedContentScriptFixOrigAjaxOptionsOnSuccess) {
 57        ajaxContext._ajaxLoadedContentScriptFixOrigAjaxOptionsOnSuccess(ajaxContext);
 58    }
 59}
 60
 61Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._globalEvalScriptInElementId = function GlobalEvalScriptInElementId(element) {
 62    if (jQuery) {
 63        // jQuery.globalEval($("#" + element.id).find("script").text());
 64
 65        // It seems jQuery 1.4.1 &amp; 1.4.2 has a problem in IE with .text() on script nodes, so do the loop ourselves
 66        var scripts = $("#" + element.id).find("script");
 67        var allScriptText = "";
 68        for (var i = 0; i < scripts.length; i++) {
 69            allScriptText += scripts[i].text;
 70        }
 71        jQuery.globalEval(allScriptText);
 72
 73    } else {
 74        alert("Error: jQuery must be loaded in order to use Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix");
 75    }
 76}
 77
 78Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._reinitialiseMvcValidation = function Sys_Mvc_MvcHelpers_AjaxLoadedContentScriptFix$ReinitialiseMvcValidation() {
 79    if (Sys.Mvc.FormContext) {
 80        Sys.Application.remove_load(arguments.callee);
 81        Sys.Mvc.FormContext._Application_Load();
 82    }
 83}
 84
 85// Register this extension
 86Sys.Application.add_load(function () {
 87
 88    if (typeof (Sys.Mvc) === 'undefined' || typeof (Sys.Mvc.MvcHelpers) === 'undefined' ||
 89        (!Sys.Mvc.MvcHelpers._onComplete &amp;&amp; !Sys.Mvc.MvcHelpers.$3)) alert("Error: MicrosoftAjax and MicrosoftMvcAjax.js (or their debug versions) must be loaded in order to use Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix");
 90
 91    var isMicrosoftMvcAjaxDebugJs = Sys.Mvc.MvcHelpers._onComplete;
 92
 93    if (isMicrosoftMvcAjaxDebugJs) { // if using MicrosoftMvcAjax.Debug.js
 94        Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._origOnComplete = Sys.Mvc.MvcHelpers._onComplete;
 95        Sys.Mvc.MvcHelpers._onComplete = Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._onComplete;
 96    } else { // using MicrosoftMvcAjax.js
 97        Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._origOnComplete = Sys.Mvc.MvcHelpers.$3;
 98        Sys.Mvc.MvcHelpers.$3 = Sys.Mvc.MvcHelpers.AjaxLoadedContentScriptFix._onComplete;
 99    }
100});

Note: this same technique could be used to hook into the other AjaxOption delegate methods if there was a standard, application-wide action that should take place on each AJAX operation… oh I don’t know… for instance: error handling… or an animation…

Update 05/07/2010 - fixed an issue where the script only worked when using the MicrosoftMvcAjax.Debug.js and did not work when using MicrosoftMvcAjax.js. Warning: this script may not work with future versions of the minimised MicrosoftMvcAjax.js.

Update 07/07/2010 - slight tweak to make the check for Sys.Mvc.MvcHelpers happen in the load, and not before we actually need to do it. Some older browsers load the js files out of order and the alert was unnecessarily showing.