jeudi 16 janvier 2020

Generate jwt web auth tokens in ember-cli-mirage (or any JavaScript) for use in your ember app

I work on an Ember team that implemented djangorestframework-simplejwt for our API security. It's a good API solution, but our mirage user was getting logged out after a period of time and could not log back into our app (for testing, development). I traced the problem down to how jwt works, and the fact that I had pasted static jwt tokens in our mirage config /login endpoint.

jwt or JavaScript Web Tokens contain an expiration date, set on the server. Once that expiration date passes, the client cannot be auth'ed into the app anymore, until the server sends a new token with a future expiration date. This was a problem for our mirage ENV, because the mirage endpoint for /login was returning a static jwt token which I had copy/pasted from our backend response. The workaround was to get new tokens from our backend, paste them into our mirage config and use them until they expire, which is not a true permanent solution to this problem.

After a LOT of trial and error (and learning way too much about jwt), I came up with this solution, which creates a valid jwt token with an expiration date 7 days in the future. It only requires crypto-js (npm install crypto-js), a very lightweight library with many crypto functions, but no dependencies:

import CryptoJS from 'CryptoJS';

const generateTokens = function(secretKey) { // fn to generate jwt access and refresh tokens with live date (+7 days) expiration
  let newEpochDate = new Date().valueOf();
  newEpochDate += 6.048e8; // add 7 days padding
  newEpochDate = Math.trunc(newEpochDate / 1000); // convert to Java epoch date value
  let tokenObjBase = {
    'typ': 'JWT',
    'alg': 'HS256'
  };
  let tokenObjAccess = {
    'token_type': 'access',
    'exp': newEpochDate,
    'jti': '83bc20a2fb564aa8937d167586166f67',
    'user_id': 24865
  };
  let tokenObjRefresh = {
    'token_type': 'refresh',
    'exp': newEpochDate,
    'jti': '83bc20a2fb564aa8937d167586166f67',
    'user_id': 24865
  };

  let base64urlEncode = function (obj) {
    let base64url = CryptoJS.enc.Utf8.parse(JSON.stringify(obj)).toString(CryptoJS.enc.Base64);
    base64url = base64url.replace(/=/g, '').replace(/\//g, '_').replace(/\+/g, '-'); // crypto-js doesn't have base64url encoding; we must manually make the tokens URL safe
    return base64url;
  }
  let tokenBase = base64urlEncode(tokenObjBase);
  let tokenAccess = base64urlEncode(tokenObjAccess);
  let tokenRefresh = base64urlEncode(tokenObjRefresh);

  let signatureAccessArray = CryptoJS.HmacSHA256(tokenBase + '.' + tokenAccess, secretKey); // crypto-js returns a "wordarray" which must be stringified back to human readable text with a specific encoding
  let signatureAccess = signatureAccessArray.toString(CryptoJS.enc.Base64).replace(/=+$/, '').replace(/\//g, '_').replace(/\+/g, '-'); // crypto-js doesn't have base64url encoding; we must manually make the tokens URL safe
  let signatureRefreshArray = CryptoJS.HmacSHA256(tokenBase + '.' + tokenRefresh, secretKey);
  let signatureRefresh = signatureRefreshArray.toString(CryptoJS.enc.Base64).replace(/=+$/, '').replace(/\//g, '_').replace(/\+/g, '-'); // crypto-js doesn't have base64url encoding; we must manually make the tokens URL safe

  return {tokenRefresh: tokenBase + '.' + tokenRefresh + '.' + signatureRefresh, tokenAccess: tokenBase + '.' + tokenAccess + '.' + signatureAccess};
}

// you may also need this in your ember-cli-build:
app.import('node_modules/crypto-js/crypto-js.js', {
  using: [
    { transformation: 'amd', as: 'CryptoJS' }
  ]
});

This fn lives in our mirage/config.js file, above the export default function() { opening module line so it can be called by any route in the config file: let tokens = generateTokens('thisisnotarealsecretkey');

It returns an object with an "access" token and a "refresh" token, the two token types required by our django jwt setup. Customize the tokenObjBase, tokenObjAccess and tokenObjRefresh to meet your backend's setup.

The basic structure of a jwt token can be found here: https://jwt.io/

To summarize, a jwt token has three strings, separated by two periods (.).

The first string is the tokenObjBase passed through JSON.stringify(), then converted to a base64URL value. That URL part is important, because regular base64 encodings don't remove the =, + and / chars, which are not "web safe." The tokenObjBase must contain typ and alg properties and nothing else.

The second string is your "payload" (here, tokenObjAccess or tokenObjRefresh) and usually contains user info (name, id, etc), and also an epoch date value which represents the expiration date of the token. That payload obj, like the first, is passed through JSON.stringify(), then converted to a base64URL value. DO NOT put sensitive data in these first two objs, they are not "encrypted" at all. Base64 encoding can be reversed by anyone with a computer and Google.

The third string is the jwt "signature." It is created by concatenating the first two base64 strings with a period (.) in the middle, then passing them through the HS256 encryption algorithm (HMAC-SHA256).

Then all three strings (two base64URL strings and the HS256 encrypted string) are concatenated: base64URL(tokenObjBase) + '.' + base64URL(tokenObjPayload) + '.' + signatureHS256

Hope this helps anyone having issues with jwt permanently logging their mirage users out of their Ember applications!




Aucun commentaire:

Enregistrer un commentaire