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