var pl = { TOKEN_KEY: "pl_jwt_token", PATH_LOGIN: "/auth/login", PATH_STATUS: "/auth/status", PATH_LOGOUT: "/auth/logout", loggedIn: undefined, token: null, basePath: "", getToken: function() { var token = window.localStorage.getItem(pl.TOKEN_KEY); if (token == null) { token = pl.token; } return token; }, setToken: function(token) { if (token == null || token == undefined) { pl.clearToken(); } else { window.localStorage.setItem(pl.TOKEN_KEY, token); } pl.token = token; }, clearToken: function() { window.localStorage.removeItem(pl.TOKEN_KEY); }, createRequestObject: function(method, path, callback, failCallback) { var r; if (window.XMLHttpRequest) { r = new XMLHttpRequest(); } else { r = new ActiveXObject("Microsoft.XMLHTTP"); } r.onreadystatechange = function() { if (r.readyState == 4) { if (r.status >= 200 && r.status <= 299) { // Done to avoid a memory leak window.setTimeout(function() { r.onreadystatechange = function() {}; }, 0); if (callback) { callback(r); } } else if (r.status == 401) { // unauthorized // Done to avoid a memory leak window.setTimeout(function() { r.onreadystatechange = function() {}; }, 0); if (failCallback) { failCallback(r); } } } } r.open(method, path, true); r.setRequestHeader("X-Requested-With", "XMLHttpRequest"); return r; }, status: function(callback) { var cb = function(response) { var content = response.responseText; if (typeof content == "string" && content.length > 0) { var valid = JSON.parse(content); if (valid) { pl.loggedIn = true; } else { pl.loggedIn = false; } } if (callback) { callback.call(); } }; var cbFail = function() { pl.loggedIn = false; if (callback) { callback.call(); } } var r = pl.createRequestObject("GET", pl.basePath + pl.PATH_STATUS, cb, cbFail); if (pl.getToken() != null) { r.setRequestHeader("Authorization", "Token " + pl.getToken()); } r.send(); }, login: function(username, password, callback) { var cb = function(response) { var content = response.responseText; if (typeof content == "string" && content.length > 0) { var acct = JSON.parse(content); if (acct != null) { pl.loggedIn = true; var wt = pl.jwt.WebTokenParser.parse(acct.authctoken); var payload = JSON.parse(pl.jwt.base64urldecode(wt.payloadSegment)); // pl.token = payload.jti; pl.setToken(acct.authctoken); if (callback) { callback.call(); } } } }; var r = pl.createRequestObject("POST", pl.basePath + pl.PATH_LOGIN, cb); r.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password)); r.send(); }, logout: function(callback) { var cb = function(response) { var content = response.responseText; if (typeof content == "string" && content.length > 0) { var result = JSON.parse(content); if (result == true) { pl.loggedIn = false; pl.clearToken(); if (callback) { callback.call(); } } } } var r = pl.createRequestObject("GET", pl.basePath + pl.PATH_LOGOUT, cb); if (pl.getToken() != null) { r.setRequestHeader("Authorization", "Token " + pl.getToken()); } r.send(); } }; // // The following code adopted from https://github.com/michaelrhanson/jwt-js // //jwt-js - JSON Web Tokens implemented in pure JavaScript // //authors: // Michael Hanson // //Unless otherwise indicated, all code in this project is covered by the //MPL1.1/GPL2.0/LGPL2.1 trilicense included at the end of this file. // pl.jwt = { // convert a base64url string to hex b64urlmap: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", b64urltohex: function(s) { var ret = "" var i; var k = 0; // b64 state, 0-3 var slop; for(i = 0; i < s.length; ++i) { var v = b64urlmap.indexOf(s.charAt(i)); if(v < 0) continue; if(k == 0) { ret += int2char(v >> 2); slop = v & 3; k = 1; } else if(k == 1) { ret += int2char((slop << 2) | (v >> 4)); slop = v & 0xf; k = 2; } else if(k == 2) { ret += int2char(slop); ret += int2char(v >> 2); slop = v & 3; k = 3; } else { ret += int2char((slop << 2) | (v >> 4)); ret += int2char(v & 0xf); k = 0; } } if(k == 1) ret += int2char(slop << 2); return ret; }, base64urlencode: function(arg) { var s = window.btoa(arg); // Standard base64 encoder s = s.split('=')[0]; // Remove any trailing '='s s = s.replace(/\+/g, '-'); // 62nd char of encoding s = s.replace(/\//g, '_'); // 63rd char of encoding // TODO optimize this; we can do much better return s; }, base64urldecode: function(arg) { var s = arg; s = s.replace(/-/g, '+'); // 62nd char of encoding s = s.replace(/_/g, '/'); // 63rd char of encoding switch (s.length % 4) // Pad with trailing '='s { case 0: break; // No pad chars in this case case 2: s += "=="; break; // Two pad chars case 3: s += "="; break; // One pad char default: throw new InputException("Illegal base64url string!"); } return window.atob(s); // Standard base64 decoder }, NoSuchAlgorithmException: function(message) { this.message = message; this.toString = function() { return "No such algorithm: "+this.message; }; }, NotImplementedException: function(message) { this.message = message; this.toString = function() { return "Not implemented: "+this.message; }; }, InputException: function(message) { this.message = message; this.toString = function() { return "Malformed input: "+this.message; }; }, jsonObj: function(strOrObject) { if (typeof strOrObject == "string") { return JSON.parse(strOrObject); } return strOrObject; }, constructAlgorithm: function(jwtAlgStr, key) { if ("ES256" === jwtAlgStr) { throw new NotImplementedException("ECDSA-SHA256 not yet implemented"); } else if ("ES384" === jwtAlgStr) { throw new NotImplementedException("ECDSA-SHA384 not yet implemented"); } else if ("ES512" === jwtAlgStr) { throw new NotImplementedException("ECDSA-SHA512 not yet implemented"); } else if ("HS256" === jwtAlgStr) { return new HMACAlgorithm("sha256", key); } else if ("HS384" === jwtAlgStr) { throw new NotImplementedException("HMAC-SHA384 not yet implemented"); } else if ("HS512" === jwtAlgStr) { throw new NotImplementedException("HMAC-SHA512 not yet implemented"); } else if ("RS256" === jwtAlgStr) { return new RSASHAAlgorithm("sha256", key); } else if ("RS384" === jwtAlgStr) { throw new NotImplementedException("RSA-SHA384 not yet implemented"); } else if ("RS512" === jwtAlgStr) { throw new NotImplementedException("RSA-SHA512 not yet implemented"); } else { throw new NoSuchAlgorithmException("Unknown algorithm: " + jwtAlgStr); } }, }; pl.jwt.WebToken = function(objectStr, algorithm) { this.objectStr = objectStr; this.pkAlgorithm = algorithm; }; pl.jwt.WebToken.prototype = { serialize: function(key) { var header = pl.jwt.jsonObj(this.pkAlgorithm); var jwtAlgStr = header.alg; var algorithm = constructAlgorithm(jwtAlgStr, key); var algBytes = pl.jwt.base64urlencode(this.pkAlgorithm); var jsonBytes = pl.jwt.base64urlencode(this.objectStr); var stringToSign = algBytes + "." + jsonBytes; algorithm.update(stringToSign); var digestValue = algorithm.finalize(); var signatureValue = algorithm.sign(); return algBytes + "." + jsonBytes + "." + signatureValue; }, verify: function(key) { var header = pl.jwt.jsonObj(this.pkAlgorithm); var jwtAlgStr = header.alg; var algorithm = constructAlgorithm(jwtAlgStr, key); algorithm.update(this.headerSegment + "." + this.payloadSegment); algorithm.finalize(); return algorithm.verify(this.cryptoSegment); } }; pl.jwt.WebTokenParser = { parse: function(input) { var parts = input.split("."); if (parts.length != 3) { throw new MalformedWebToken("Must have three parts"); } var token = new pl.jwt.WebToken(); token.headerSegment = parts[0]; token.payloadSegment = parts[1]; token.cryptoSegment = parts[2]; token.pkAlgorithm = pl.jwt.base64urldecode(parts[0]); return token; } }; pl.jwt.HMACAlgorithm = function(hash, key) { if (hash == "sha256") { this.hash = sjcl.hash.sha256; } else { throw new NoSuchAlgorithmException("HMAC does not support hash " + hash); } this.key = sjcl.codec.utf8String.toBits(key); }; pl.jwt.HMACAlgorithm.prototype = { update: function(data) { this.data = data; }, finalize: function() { }, sign: function() { var hmac = new sjcl.misc.hmac(this.key, this.hash); var result = hmac.encrypt(this.data); return pl.jwt.base64urlencode(window.atob(sjcl.codec.base64.fromBits(result))); }, verify: function(sig) { var hmac = new sjcl.misc.hmac(this.key, this.hash); var result = hmac.encrypt(this.data); return pl.jwt.base64urlencode(window.atob(sjcl.codec.base64.fromBits(result))) == sig; } }; pl.jwt.RSASHAAlgorithm = function(hash, keyPEM) { if (hash == "sha1") { this.hash = "sha1"; } else if (hash == "sha256") { this.hash = "sha256"; } else { throw new NoSuchAlgorithmException("JWT algorithm: " + hash); } this.keyPEM = keyPEM; }; pl.jwt.RSASHAAlgorithm.prototype = { update: function(data) { this.data = data; }, finalize: function() { }, sign: function() { var rsa = new RSAKey(); rsa.readPrivateKeyFromPEMString(this.keyPEM); var hSig = rsa.signString(this.data, this.hash); return pl.jwt.base64urlencode(pl.jwt.base64urldecode(hex2b64(hSig))); // TODO replace this with hex2b64urlencode! }, verify: function(sig) { var result = this.keyPEM.verifyString(this.data, pl.jwt.b64urltohex(sig)); return result; } };