diff --git a/gulpfile.js b/gulpfile.js index 08ad2a7b..ab591af8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -166,9 +166,93 @@ function copy_ts(){ .pipe(gulp.dest('./dist/')); } +// Replace the babel-polyfill with specific core-js polyfills. +function slimBuildTarget() { + return { + mode: 'production', + entry: ['./polyfills.js', './index.js'], + output: { + filename: 'oidc-client.slim.min.js', + libraryTarget: 'var', + library: 'Oidc' + }, + plugins: [], + optimization: { + minimizer: [ + new UglifyJsPlugin({ + uglifyOptions: { + compress: { + keep_fnames: true + } + } + }) + ] + } + }; +} +function slimBuildTargetSourceMap() { + return { + mode: 'development', + entry: ['./polyfills.js', './index.js'], + output: { + filename: 'oidc-client.slim.js', + libraryTarget: 'var', + library: 'Oidc' + }, + plugins: [], + devtool:'inline-source-map' + }; +} + +// Adds a configuration for slimming down the production build. This build +// does not contain the full babel-polyfill. Instead it imports specific +// core-js polyfills +function build_dist_slim() { + return gulp.src('index.js') + .pipe(webpackStream(createWebpackConfig(slimBuildTarget()), webpack)) + .pipe(gulp.dest('dist/')); +}; +function build_dist_slim_sourcemap() { + return gulp.src('index.js') + .pipe(webpackStream(createWebpackConfig(slimBuildTargetSourceMap()), webpack)) + .pipe(gulp.dest('dist/')); +}; + +// Creates a build with only RSA256 exponent+modulus support (no X509) +function build_dist_slim_rsa() { + var conf = slimBuildTarget(); + conf.output.filename = 'oidc-client.rsa256.slim.min.js'; + + // This plugin should always be first in the chain + conf.plugins.unshift( + new webpack.NormalModuleReplacementPlugin(/(.*)JoseUtil(\.js)?$/, (resource) => { + resource.request = resource.request.replace(/JoseUtil/, 'JoseUtilRsa'); + }) + ); + + return gulp.src('index.js') + .pipe(webpackStream(createWebpackConfig(conf), webpack)) + .pipe(gulp.dest('dist/')); +}; +function build_dist_slim_rsa_sourcemap() { + var conf = slimBuildTargetSourceMap(); + conf.output.filename = 'oidc-client.rsa256.slim.js'; + + // This plugin should always be first in the chain + conf.plugins.unshift( + new webpack.NormalModuleReplacementPlugin(/(.*)JoseUtil(\.js)?$/, (resource) => { + resource.request = resource.request.replace(/JoseUtil/, 'JoseUtilRsa'); + }) + ); + + return gulp.src('index.js') + .pipe(webpackStream(createWebpackConfig(conf), webpack)) + .pipe(gulp.dest('dist/')); +}; + // putting it all together exports.default = gulp.series( build_jsrsasign, - gulp.parallel(build_lib_sourcemap, build_lib_min, build_dist_sourcemap, build_dist_min), + gulp.parallel(build_lib_sourcemap, build_lib_min, build_dist_sourcemap, build_dist_min, build_dist_slim, build_dist_slim_rsa, build_dist_slim_sourcemap, build_dist_slim_rsa_sourcemap), copy_ts ); diff --git a/package-lock.json b/package-lock.json index d1da551f..861db978 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1591,6 +1591,12 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", + "dev": true + }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", @@ -1857,9 +1863,9 @@ } }, "core-js": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz", - "integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs=", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.4.tgz", + "integrity": "sha512-05qQ5hXShcqGkPZpXEFLIpxayZscVD2kuMBZewxiIPPEagukO4mqgPA9CWhUvFBJfy3ODdK2p9xyHh7FTU9/7A==", "dev": true }, "core-util-is": { @@ -1947,6 +1953,12 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "3.1.9-1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", + "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=", + "dev": true + }, "cyclist": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", @@ -4357,6 +4369,12 @@ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", "dev": true }, + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=", + "dev": true + }, "jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", diff --git a/package.json b/package.json index c675f054..f6152c0b 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,15 @@ "babel-polyfill": "^6.9.1", "babel-preset-es2015": "^6.6.0", "babel-register": "^6.7.2", + "base64-js": "^1.3.0", "chai": "^4.2.0", + "core-js": "^2.6.4", + "crypto-js": "^3.1.9-1", "express": "^4.16.4", "gulp": "^4.0.0", "gulp-concat": "^2.6.1", "gulp-rename": "^1.4.0", + "jsbn": "^1.1.0", "jsrsasign": "^8.0.12", "mocha": "^5.2.0", "natives": "^1.1.6", diff --git a/polyfills.js b/polyfills.js new file mode 100644 index 00000000..2f2031e9 --- /dev/null +++ b/polyfills.js @@ -0,0 +1,14 @@ +// Declare the ES6 features we're using + +// TODO: Consider using the local function versions of these, so that we +// avoid modifying browser globals (potential for interop bugs with other libraries +// on the page that might be polyfilling ES6 features) + +import 'core-js/es6/promise'; +import 'core-js/fn/function/bind'; +import 'core-js/fn/object/assign'; +import 'core-js/fn/object/assign'; +import 'core-js/fn/array/find'; +import 'core-js/fn/array/some'; +import 'core-js/fn/array/is-array'; +import 'core-js/fn/array/splice'; diff --git a/src/JoseUtil.js b/src/JoseUtil.js index d213f9c4..5dd5350d 100644 --- a/src/JoseUtil.js +++ b/src/JoseUtil.js @@ -1,165 +1,4 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +import { jws, KeyUtil, X509, crypto, hextob64u, b64tohex, AllowedSigningAlgs } from './crypto/jsrsasign'; +import getJoseUtil from './JoseUtilImpl'; -import { jws, KEYUTIL as KeyUtil, X509, crypto, hextob64u, b64tohex } from '../jsrsasign/dist/jsrsasign.js'; -//import { jws, KEYUTIL as KeyUtil, X509, crypto, hextob64u, b64tohex } from 'jsrsasign'; -import { Log } from './Log.js'; - -const AllowedSigningAlgs = ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512']; - -export class JoseUtil { - - static parseJwt(jwt) { - Log.debug("JoseUtil.parseJwt"); - try { - var token = jws.JWS.parse(jwt); - return { - header: token.headerObj, - payload: token.payloadObj - } - } - catch (e) { - Log.error(e); - } - } - - static validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive) { - Log.debug("JoseUtil.validateJwt"); - - try { - if (key.kty === "RSA") { - if (key.e && key.n) { - key = KeyUtil.getKey(key); - } - else if (key.x5c && key.x5c.length) { - var hex = b64tohex(key.x5c[0]); - key = X509.getPublicKeyFromCertHex(hex); - } - else { - Log.error("JoseUtil.validateJwt: RSA key missing key material", key); - return Promise.reject(new Error("RSA key missing key material")); - } - } - else if (key.kty === "EC") { - if (key.crv && key.x && key.y) { - key = KeyUtil.getKey(key); - } - else { - Log.error("JoseUtil.validateJwt: EC key missing key material", key); - return Promise.reject(new Error("EC key missing key material")); - } - } - else { - Log.error("JoseUtil.validateJwt: Unsupported key type", key && key.kty); - return Promise.reject(new Error("Unsupported key type: " + key && key.kty)); - } - - return JoseUtil._validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive); - } - catch (e) { - Log.error(e && e.message || e); - return Promise.reject("JWT validation failed"); - } - } - - static validateJwtAttributes(jwt, issuer, audience, clockSkew, now, timeInsensitive) { - if (!clockSkew) { - clockSkew = 0; - } - - if (!now) { - now = parseInt(Date.now() / 1000); - } - - var payload = JoseUtil.parseJwt(jwt).payload; - - if (!payload.iss) { - Log.error("JoseUtil._validateJwt: issuer was not provided"); - return Promise.reject(new Error("issuer was not provided")); - } - if (payload.iss !== issuer) { - Log.error("JoseUtil._validateJwt: Invalid issuer in token", payload.iss); - return Promise.reject(new Error("Invalid issuer in token: " + payload.iss)); - } - - if (!payload.aud) { - Log.error("JoseUtil._validateJwt: aud was not provided"); - return Promise.reject(new Error("aud was not provided")); - } - var validAudience = payload.aud === audience || (Array.isArray(payload.aud) && payload.aud.indexOf(audience) >= 0); - if (!validAudience) { - Log.error("JoseUtil._validateJwt: Invalid audience in token", payload.aud); - return Promise.reject(new Error("Invalid audience in token: " + payload.aud)); - } - if (payload.azp && payload.azp !== audience) { - Log.error("JoseUtil._validateJwt: Invalid azp in token", payload.azp); - return Promise.reject(new Error("Invalid azp in token: " + payload.azp)); - } - - if (!timeInsensitive) { - var lowerNow = now + clockSkew; - var upperNow = now - clockSkew; - - if (!payload.iat) { - Log.error("JoseUtil._validateJwt: iat was not provided"); - return Promise.reject(new Error("iat was not provided")); - } - if (lowerNow < payload.iat) { - Log.error("JoseUtil._validateJwt: iat is in the future", payload.iat); - return Promise.reject(new Error("iat is in the future: " + payload.iat)); - } - - if (payload.nbf && lowerNow < payload.nbf) { - Log.error("JoseUtil._validateJwt: nbf is in the future", payload.nbf); - return Promise.reject(new Error("nbf is in the future: " + payload.nbf)); - } - - if (!payload.exp) { - Log.error("JoseUtil._validateJwt: exp was not provided"); - return Promise.reject(new Error("exp was not provided")); - } - if (payload.exp < upperNow) { - Log.error("JoseUtil._validateJwt: exp is in the past", payload.exp); - return Promise.reject(new Error("exp is in the past:" + payload.exp)); - } - } - - return Promise.resolve(payload); - } - - static _validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive) { - - return JoseUtil.validateJwtAttributes(jwt, issuer, audience, clockSkew, now, timeInsensitive).then(payload => { - try { - if (!jws.JWS.verify(jwt, key, AllowedSigningAlgs)) { - Log.error("JoseUtil._validateJwt: signature validation failed"); - return Promise.reject(new Error("signature validation failed")); - } - - return payload; - } - catch (e) { - Log.error(e && e.message || e); - return Promise.reject(new Error("signature validation failed")); - } - }); - } - - static hashString(value, alg) { - try { - return crypto.Util.hashString(value, alg); - } - catch (e) { - Log.error(e); - } - } - - static hexToBase64Url(value) { - try { - return hextob64u(value); - } - catch (e) { - Log.error(e); - } - } -} +export const JoseUtil = getJoseUtil({ jws, KeyUtil, X509, crypto, hextob64u, b64tohex, AllowedSigningAlgs }); diff --git a/src/JoseUtilImpl.js b/src/JoseUtilImpl.js new file mode 100644 index 00000000..e2cf383d --- /dev/null +++ b/src/JoseUtilImpl.js @@ -0,0 +1,153 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log } from './Log.js'; + +export default function getJoseUtil({ jws, KeyUtil, X509, crypto, hextob64u, b64tohex, AllowedSigningAlgs }) { + return class JoseUtil { + + static parseJwt(jwt) { + Log.debug("JoseUtil.parseJwt"); + try { + var token = jws.JWS.parse(jwt); + return { + header: token.headerObj, + payload: token.payloadObj + } + } catch (e) { + Log.error(e); + } + } + + static validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive) { + Log.debug("JoseUtil.validateJwt"); + + try { + if (key.kty === "RSA") { + if (key.e && key.n) { + key = KeyUtil.getKey(key); + } else if (key.x5c && key.x5c.length) { + var hex = b64tohex(key.x5c[0]); + key = X509.getPublicKeyFromCertHex(hex); + } else { + Log.error("JoseUtil.validateJwt: RSA key missing key material", key); + return Promise.reject(new Error("RSA key missing key material")); + } + } else if (key.kty === "EC") { + if (key.crv && key.x && key.y) { + key = KeyUtil.getKey(key); + } else { + Log.error("JoseUtil.validateJwt: EC key missing key material", key); + return Promise.reject(new Error("EC key missing key material")); + } + } else { + Log.error("JoseUtil.validateJwt: Unsupported key type", key && key.kty); + return Promise.reject(new Error("Unsupported key type: " + key && key.kty)); + } + + return JoseUtil._validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive); + } catch (e) { + Log.error(e && e.message || e); + return Promise.reject("JWT validation failed"); + } + } + + static validateJwtAttributes(jwt, issuer, audience, clockSkew, now, timeInsensitive) { + if (!clockSkew) { + clockSkew = 0; + } + + if (!now) { + now = parseInt(Date.now() / 1000); + } + + var payload = JoseUtil.parseJwt(jwt).payload; + + if (!payload.iss) { + Log.error("JoseUtil._validateJwt: issuer was not provided"); + return Promise.reject(new Error("issuer was not provided")); + } + if (payload.iss !== issuer) { + Log.error("JoseUtil._validateJwt: Invalid issuer in token", payload.iss); + return Promise.reject(new Error("Invalid issuer in token: " + payload.iss)); + } + + if (!payload.aud) { + Log.error("JoseUtil._validateJwt: aud was not provided"); + return Promise.reject(new Error("aud was not provided")); + } + var validAudience = payload.aud === audience || (Array.isArray(payload.aud) && payload.aud.indexOf(audience) >= 0); + if (!validAudience) { + Log.error("JoseUtil._validateJwt: Invalid audience in token", payload.aud); + return Promise.reject(new Error("Invalid audience in token: " + payload.aud)); + } + if (payload.azp && payload.azp !== audience) { + Log.error("JoseUtil._validateJwt: Invalid azp in token", payload.azp); + return Promise.reject(new Error("Invalid azp in token: " + payload.azp)); + } + + if (!timeInsensitive) { + var lowerNow = now + clockSkew; + var upperNow = now - clockSkew; + + if (!payload.iat) { + Log.error("JoseUtil._validateJwt: iat was not provided"); + return Promise.reject(new Error("iat was not provided")); + } + if (lowerNow < payload.iat) { + Log.error("JoseUtil._validateJwt: iat is in the future", payload.iat); + return Promise.reject(new Error("iat is in the future: " + payload.iat)); + } + + if (payload.nbf && lowerNow < payload.nbf) { + Log.error("JoseUtil._validateJwt: nbf is in the future", payload.nbf); + return Promise.reject(new Error("nbf is in the future: " + payload.nbf)); + } + + if (!payload.exp) { + Log.error("JoseUtil._validateJwt: exp was not provided"); + return Promise.reject(new Error("exp was not provided")); + } + if (payload.exp < upperNow) { + Log.error("JoseUtil._validateJwt: exp is in the past", payload.exp); + return Promise.reject(new Error("exp is in the past:" + payload.exp)); + } + } + + return Promise.resolve(payload); + } + + static _validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive) { + + return JoseUtil.validateJwtAttributes(jwt, issuer, audience, clockSkew, now, timeInsensitive).then(payload => { + try { + if (!jws.JWS.verify(jwt, key, AllowedSigningAlgs)) { + Log.error("JoseUtil._validateJwt: signature validation failed"); + return Promise.reject(new Error("signature validation failed")); + } + + return payload; + } catch (e) { + Log.error(e && e.message || e); + return Promise.reject(new Error("signature validation failed")); + } + }); + } + + static hashString(value, alg) { + try { + return crypto.Util.hashString(value, alg); + } catch (e) { + Log.error(e); + } + } + + static hexToBase64Url(value) { + try { + return hextob64u(value); + } catch (e) { + Log.error(e); + } + } + } +} diff --git a/src/JoseUtilRsa.js b/src/JoseUtilRsa.js new file mode 100644 index 00000000..8baaa240 --- /dev/null +++ b/src/JoseUtilRsa.js @@ -0,0 +1,4 @@ +import { jws, KeyUtil, X509, crypto, hextob64u, b64tohex, AllowedSigningAlgs } from './crypto/rsa'; +import getJoseUtil from './JoseUtilImpl'; + +export const JoseUtil = getJoseUtil({ jws, KeyUtil, X509, crypto, hextob64u, b64tohex, AllowedSigningAlgs }); diff --git a/src/crypto/jsrsasign.js b/src/crypto/jsrsasign.js new file mode 100644 index 00000000..b6ab7608 --- /dev/null +++ b/src/crypto/jsrsasign.js @@ -0,0 +1,13 @@ +import { jws, KEYUTIL as KeyUtil, X509, crypto, hextob64u, b64tohex } from '../../jsrsasign/dist/jsrsasign.js'; + +const AllowedSigningAlgs = ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512']; + +export { + jws, + KeyUtil, + X509, + crypto, + hextob64u, + b64tohex, + AllowedSigningAlgs +}; diff --git a/src/crypto/rsa.js b/src/crypto/rsa.js new file mode 100644 index 00000000..3ead0869 --- /dev/null +++ b/src/crypto/rsa.js @@ -0,0 +1,293 @@ +/* +Based on the work of Auth0 +https://github.com/auth0/idtoken-verifier +https://github.com/auth0/idtoken-verifier/blob/master/LICENSE +Which is based on the work of Tom Wu +http://www-cs-students.stanford.edu/~tjw/jsbn/ +http://www-cs-students.stanford.edu/~tjw/jsbn/LICENSE +*/ + +/* + * To support most basic OpenId use cases (using RSA256), we can get away without + * requiring the full jrsasign feature set (and resulting massive bundle). + * + * - Support RSA 256 algorithm (optionally could support RSA* family) + * - Parse JWT tokens using the (n) parameter. + * - Verify signature of id_tokens + * - Verify at_hash of access_tokens + * - Perform common base64 encoding/decoding tasks. + */ + +import { BigInteger } from 'jsbn'; +import SHA256 from 'crypto-js/sha256'; +import base64Js from 'base64-js'; + +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +var b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +var b64pad = "="; + +const Base64 = { + b64tohex(s) { + var ret = ""; + var i; + var k = 0; // b64 state, 0-3 + var slop; + for(i = 0; i < s.length; ++i) { + if(s.charAt(i) === b64pad) break; + var v = b64map.indexOf(s.charAt(i)); + if(v < 0) continue; + if(k === 0) { + ret += String.fromCharCode(v >> 2); + slop = v & 3; + k = 1; + } + else if(k === 1) { + ret += String.fromCharCode((slop << 2) | (v >> 4)); + slop = v & 0xf; + k = 2; + } + else if(k === 2) { + ret += String.fromCharCode(slop); + ret += String.fromCharCode(v >> 2); + slop = v & 3; + k = 3; + } + else { + ret += String.fromCharCode((slop << 2) | (v >> 4)); + ret += String.fromCharCode(v & 0xf); + k = 0; + } + } + if(k === 1) + ret += String.fromCharCode(slop << 2); + return ret; + }, + hexToBase64(h) { + var i; + var c; + var ret = ""; + for(i = 0; i+3 <= h.length; i+=3) { + c = parseInt(h.substring(i,i+3),16); + ret += b64map.charAt(c >> 6) + b64map.charAt(c & 63); + } + if(i+1 === h.length) { + c = parseInt(h.substring(i,i+1),16); + ret += b64map.charAt(c << 2); + } + else if(i+2 === h.length) { + c = parseInt(h.substring(i,i+2),16); + ret += b64map.charAt(c >> 2) + b64map.charAt((c & 3) << 4); + } + if (b64pad) while((ret.length & 3) > 0) ret += b64pad; + return ret; + }, + + padding(str) { + var mod = (str.length % 4); + var pad = 4 - mod; + + if (mod === 0) { + return str; + } + + return str + (new Array(1 + pad)).join('='); + }, + + byteArrayToHex(raw) { + var HEX = ''; + + for (var i = 0; i < raw.length; i++) { + var _hex = raw[i].toString(16); + HEX += (_hex.length === 2 ? _hex : '0' + _hex); + } + + return HEX; + }, + + decodeToHEX(str) { + return Base64.byteArrayToHex(base64Js.toByteArray(Base64.padding(str))); + }, + + base64ToBase64Url(s) { + s = s.replace(/=/g, ""); + s = s.replace(/\+/g, "-"); + s = s.replace(/\//g, "_"); + return s; + }, + + urlDecode(str) { + str = str.replace(/-/g, '+') // Convert '-' to '+' + .replace(/_/g, '/') // Convert '_' to '/' + .replace(/\s/g, ' '); // Convert '\s' to ' ' + + return atob(str); + } +}; + + +var DigestInfoHead = { + sha1: '3021300906052b0e03021a05000414', + sha224: '302d300d06096086480165030402040500041c', + sha256: '3031300d060960864801650304020105000420', + sha384: '3041300d060960864801650304020205000430', + sha512: '3051300d060960864801650304020305000440', + md2: '3020300c06082a864886f70d020205000410', + md5: '3020300c06082a864886f70d020505000410', + ripemd160: '3021300906052b2403020105000414' +}; + +var DigestAlgs = { + sha256: SHA256, + SHA256:SHA256 +}; + +function RSAVerifier(modulus, exp) { + this.n = null; + this.e = 0; + + if (modulus != null && exp != null && modulus.length > 0 && exp.length > 0) { + this.n = new BigInteger(modulus, 16); + this.e = parseInt(exp, 16); + } else { + throw new Error('Invalid key data'); + } +} + +function getAlgorithmFromDigest(hDigestInfo) { + for (var algName in DigestInfoHead) { + var head = DigestInfoHead[algName]; + var len = head.length; + + if (hDigestInfo.substring(0, len) === head) { + return { + alg: algName, + hash: hDigestInfo.substring(len) + }; + } + } + return []; +} + + +RSAVerifier.prototype.verify = function (msg, encsig) { + encsig = Base64.decodeToHEX(encsig); + encsig = encsig.replace(/[^0-9a-f]|[\s\n]]/ig, ''); + + var sig = new BigInteger(encsig, 16); + + if (sig.bitLength() > this.n.bitLength()) { + throw new Error('Signature does not match with the key modulus.'); + } + + var decryptedSig = sig.modPowInt(this.e, this.n); + var digest = decryptedSig.toString(16).replace(/^1f+00/, ''); + var digestInfo = getAlgorithmFromDigest(digest); + + if (digestInfo.length === 0) { + return false; + } + + if (!DigestAlgs.hasOwnProperty(digestInfo.alg)) { + throw new Error('Hashing algorithm is not supported.'); + } + + var msgHash = DigestAlgs[digestInfo.alg](msg).toString(); + return (digestInfo.hash === msgHash); +}; + +const AllowedSigningAlgs = ['RS256']; + +const jws = { + JWS: { + parse: function(token) { + var parts = token.split('.'); + var header; + var payload; + + // This diverges from Auth0's implementation, which throws rather than + // returning undefined + if (parts.length !== 3) { + return undefined; + } + + try { + header = JSON.parse(Base64.urlDecode(parts[0])); + payload = JSON.parse(Base64.urlDecode(parts[1])); + } catch (e) { + return new Error('Token header or payload is not valid JSON'); + } + + return { + headerObj: header, + payloadObj: payload, + }; + }, + verify: function(jwt, key, allowedSigningAlgs = []) { + allowedSigningAlgs.forEach((alg) => { + if (AllowedSigningAlgs.indexOf(alg) === -1) { + throw new Error('Invalid signing algorithm: ' + alg); + } + }); + var verify = new RSAVerifier(key.n, key.e); + var parts = jwt.split('.'); + + var headerAndPayload = [parts[0], parts[1]].join('.'); + return verify.verify(headerAndPayload, parts[2]); + }, + }, +}; + +const KeyUtil = { + /** + * Returns decoded keys in Hex format for use in crypto functions. + * Supports modulus/exponent-style keys. + * + * @param {object} key the security key + * @returns + */ + getKey(key) { + if (key.kty === 'RSA') { + return { + e: Base64.decodeToHEX(key.e), + n: Base64.decodeToHEX(key.n), + }; + } + + return null; + }, +}; + +const X509 = { + getPublicKeyFromCertPEM: function() { + throw new Error('Not implemented. Use the full oidc-client library if you need support for X509.'); + }, +}; + +const crypto = { + Util: { + hashString: function(value, alg) { + var hashFunc = DigestAlgs[alg]; + return hashFunc(value).toString(); + }, + } +}; + +function hextob64u(s) { + if (s.length % 2 === 1) { + s = '0' + s; + } + return Base64.base64ToBase64Url(Base64.hexToBase64(s)); +} + +const {b64tohex} = Base64; + +export { + jws, + KeyUtil, + X509, + crypto, + hextob64u, + b64tohex, + AllowedSigningAlgs +}; diff --git a/test/unit/JoseUtil.spec.js b/test/unit/JoseUtil.spec.js index a6c08075..002c38f1 100644 --- a/test/unit/JoseUtil.spec.js +++ b/test/unit/JoseUtil.spec.js @@ -1,248 +1,261 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { JoseUtil } from '../../src/JoseUtil'; +import { JoseUtil as JoseUtilRsa } from '../../src/JoseUtilRsa'; +import { JoseUtil as JoseUtilJsrsasign } from '../../src/JoseUtil'; import { Log } from '../../src/Log'; +// Node.js does not provide global atob/btoa functions that are widely supported by browsers (incl IE10) +// https://caniuse.com/#feat=atob-btoa +global.atob = (val) => { return Buffer.from(val, 'base64') }; +global.btoa = (val) => { return Buffer.from(val).toString('base64') }; + import chai from 'chai'; chai.should(); -let assert = chai.assert; let expect = chai.expect; -describe("JoseUtil", function () { - - let jwt; - let jwtFromRsa; - let rsaKey; - let ecKey; - - const expectedIssuer = "https://localhost:44333/core"; - const expectedAudience = "js.tokenmanager"; - const notBefore = 1459129901; - const issuedAt = notBefore; - const expires = 1459130201; - - const expectedNow = notBefore; - - beforeEach(function () { - Log.logger = console; - Log.level = Log.NONE; - - jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMzMy9jb3JlIiwiYXVkIjoianMudG9rZW5tYW5hZ2VyIiwiZXhwIjoxNDU5MTMwMjAxLCJuYmYiOjE0NTkxMjk5MDEsIm5vbmNlIjoiNzIyMTAwNTIwOTk3MjM4MiIsImlhdCI6MTQ1OTEyOTkwMSwiYXRfaGFzaCI6IkpnRFVDeW9hdEp5RW1HaWlXYndPaEEiLCJzaWQiOiIwYzVmMDYxZTYzOThiMWVjNmEwYmNlMmM5NDFlZTRjNSIsInN1YiI6Ijg4NDIxMTEzIiwiYXV0aF90aW1lIjoxNDU5MTI5ODk4LCJpZHAiOiJpZHNydiIsImFtciI6WyJwYXNzd29yZCJdfQ.f6S1Fdd0UQScZAFBzXwRiVsUIPQnWZLSe07kdtjANRZDZXf5A7yDtxOftgCx5W0ONQcDFVpLGPgTdhp7agZkPpCFutzmwr0Rr9G7E7mUN4xcIgAABhmRDfzDayFBEu6VM8wEWTChezSWtx2xG_2zmVJxxmNV0jvkaz0bu7iin-C_UZg6T-aI9FZDoKRGXZP9gF65FQ5pQ4bCYQxhKcvjjUfs0xSHGboL7waN6RfDpO4vvVR1Kz-PQhIRyFAJYRuoH4PdMczHYtFCb-k94r-7TxEU0vp61ww4WntbPvVWwUbCUgsEtmDzAZT-NEJVhWztNk1ip9wDPXzZ2hEhDAPJ7A"; - - jwtFromRsa = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMzMy9jb3JlIiwiYXVkIjoianMudG9rZW5tYW5hZ2VyIiwiZXhwIjoxNDU5MTMwMjAxLCJuYmYiOjE0NTkxMjk5MDEsIm5vbmNlIjoiNzIyMTAwNTIwOTk3MjM4MiIsImlhdCI6MTQ1OTEyOTkwMSwiYXRfaGFzaCI6IkpnRFVDeW9hdEp5RW1HaWlXYndPaEEiLCJzaWQiOiIwYzVmMDYxZTYzOThiMWVjNmEwYmNlMmM5NDFlZTRjNSIsInN1YiI6Ijg4NDIxMTEzIiwiYXV0aF90aW1lIjoxNDU5MTI5ODk4LCJpZHAiOiJpZHNydiIsImFtciI6WyJwYXNzd29yZCJdfQ.f6S1Fdd0UQScZAFBzXwRiVsUIPQnWZLSe07kdtjANRZDZXf5A7yDtxOftgCx5W0ONQcDFVpLGPgTdhp7agZkPpCFutzmwr0Rr9G7E7mUN4xcIgAABhmRDfzDayFBEu6VM8wEWTChezSWtx2xG_2zmVJxxmNV0jvkaz0bu7iin-C_UZg6T-aI9FZDoKRGXZP9gF65FQ5pQ4bCYQxhKcvjjUfs0xSHGboL7waN6RfDpO4vvVR1Kz-PQhIRyFAJYRuoH4PdMczHYtFCb-k94r-7TxEU0vp61ww4WntbPvVWwUbCUgsEtmDzAZT-NEJVhWztNk1ip9wDPXzZ2hEhDAPJ7A"; - - rsaKey = { - kty: "RSA", - use: "sig", - kid: "a3rMUgMFv9tPclLa6yF3zAkfquE", - x5t: "a3rMUgMFv9tPclLa6yF3zAkfquE", - e: "AQAB", - n: "qnTksBdxOiOlsmRNd-mMS2M3o1IDpK4uAr0T4_YqO3zYHAGAWTwsq4ms-NWynqY5HaB4EThNxuq2GWC5JKpO1YirOrwS97B5x9LJyHXPsdJcSikEI9BxOkl6WLQ0UzPxHdYTLpR4_O-0ILAlXw8NU4-jB4AP8Sn9YGYJ5w0fLw5YmWioXeWvocz1wHrZdJPxS8XnqHXwMUozVzQj-x6daOv5FmrHU1r9_bbp0a1GLv4BbTtSh4kMyz1hXylho0EvPg5p9YIKStbNAW9eNWvv5R8HN7PPei21AsUqxekK0oW9jnEdHewckToX7x5zULWKwwZIksll0XnVczVgy7fCFw", - x5c: [ - "MIIDBTCCAfGgAwIBAgIQNQb+T2ncIrNA6cKvUA1GWTAJBgUrDgMCHQUAMBIxEDAOBgNVBAMTB0RldlJvb3QwHhcNMTAwMTIwMjIwMDAwWhcNMjAwMTIwMjIwMDAwWjAVMRMwEQYDVQQDEwppZHNydjN0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqnTksBdxOiOlsmRNd+mMS2M3o1IDpK4uAr0T4/YqO3zYHAGAWTwsq4ms+NWynqY5HaB4EThNxuq2GWC5JKpO1YirOrwS97B5x9LJyHXPsdJcSikEI9BxOkl6WLQ0UzPxHdYTLpR4/O+0ILAlXw8NU4+jB4AP8Sn9YGYJ5w0fLw5YmWioXeWvocz1wHrZdJPxS8XnqHXwMUozVzQj+x6daOv5FmrHU1r9/bbp0a1GLv4BbTtSh4kMyz1hXylho0EvPg5p9YIKStbNAW9eNWvv5R8HN7PPei21AsUqxekK0oW9jnEdHewckToX7x5zULWKwwZIksll0XnVczVgy7fCFwIDAQABo1wwWjATBgNVHSUEDDAKBggrBgEFBQcDATBDBgNVHQEEPDA6gBDSFgDaV+Q2d2191r6A38tBoRQwEjEQMA4GA1UEAxMHRGV2Um9vdIIQLFk7exPNg41NRNaeNu0I9jAJBgUrDgMCHQUAA4IBAQBUnMSZxY5xosMEW6Mz4WEAjNoNv2QvqNmk23RMZGMgr516ROeWS5D3RlTNyU8FkstNCC4maDM3E0Bi4bbzW3AwrpbluqtcyMN3Pivqdxx+zKWKiORJqqLIvN8CT1fVPxxXb/e9GOdaR8eXSmB0PgNUhM4IjgNkwBbvWC9F/lzvwjlQgciR7d4GfXPYsE1vf8tmdQaY8/PtdAkExmbrb9MihdggSoGXlELrPA91Yce+fiRcKY3rQlNWVd4DOoJ/cPXsXwry8pWjNCo5JD8Q+RQ5yZEy7YPoifwemLhTdsBz3hlZr28oCGJ3kbnpW0xGvQb3VHSTVVbeei0CfXoW6iz1" - ] - }; - - ecKey = { - kty: "EC", - kid: "4", - use: "sig", - alg: "EC", - crv: "P-256", - x: "eZXWiRe0I3TvHPXiGnvO944gjF1o4UmitH2CVwYIrPg", - y: "AKFNss7S35tOsp5iY7-YuLGs2cLrTKFk80JvgVzMPHQ3", - x5c: [ - "MIIBpDCCAUoCgYBCs6x21IvwVHFgJxiRegyHdSiEHFur9Wj2qM5oNkv6sFbbC75L849qCgMEzdtqIhCiCnFg6PaQdswHkcclXix+y0sycyIM6l429faY3jz5eQs5SYwXYkENStzTZBsWK6u7bPiV3HvjnIv+r1af8nvO5F0tiH0TC+auDj9FgRmYljAKBggqhkjOPQQDAjAeMRwwGgYDVQQDExNUZXN0IENBIENlcnRpZmljYXRlMB4XDTEzMDIxMTIxMjQxMVoXDTE0MDIxMTIxMjQxMVowHjEcMBoGA1UEAxMTVGVzdCBDQSBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHmV1okXtCN07xz14hp7zveOIIxdaOFJorR9glcGCKz4oU2yztLfm06ynmJjv5i4sazZwutMoWTzQm+BXMw8dDcwCgYIKoZIzj0EAwIDSAAwRQIhAI4aRAoTVm3was6UimA1lFL2RId+t/fExaviosXNKg/IAiBpZB4XXcnQISwauSJ1hXNnSEcONXdqvO5gDHu+X7QHLg==" - ] - }; - }); +[JoseUtilJsrsasign, JoseUtilRsa].forEach(JoseUtil => { + const isRSA = JoseUtil === JoseUtilRsa; + const describeStr = `JoseUtil (${isRSA ? 'rsa' : 'jsrsasign'})`; - describe("parseJwt", function () { - - it("should parse a jwt", function () { - - var result = JoseUtil.parseJwt(jwt); - result.should.be.ok; - result.header.should.be.ok; - result.payload.should.be.ok; - - result.header.should.deep.equal({ - "typ": "JWT", - "alg": "RS256", - "x5t": "a3rMUgMFv9tPclLa6yF3zAkfquE", - "kid": "a3rMUgMFv9tPclLa6yF3zAkfquE" - }); - - result.payload.should.deep.equal({ - "iss": "https://localhost:44333/core", - "aud": "js.tokenmanager", - "exp": 1459130201, - "nbf": 1459129901, - "nonce": "7221005209972382", - "iat": 1459129901, - "at_hash": "JgDUCyoatJyEmGiiWbwOhA", - "sid": "0c5f061e6398b1ec6a0bce2c941ee4c5", - "sub": "88421113", - "auth_time": 1459129898, - "idp": "idsrv", - "amr": [ - "password" - ] - }); + describe(describeStr, function () { - }); + let jwt; + let jwtFromRsa; + let rsaKey; + let ecKey; - it("should return undefined for an invalid jwt", function () { + const expectedIssuer = "https://localhost:44333/core"; + const expectedAudience = "js.tokenmanager"; + const notBefore = 1459129901; + const issuedAt = notBefore; + const expires = 1459130201; - var result = JoseUtil.parseJwt("junk"); - expect(result).to.be.undefined; - }); + const expectedNow = notBefore; - }); + beforeEach(function () { + Log.logger = console; + Log.level = Log.NONE; + jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMzMy9jb3JlIiwiYXVkIjoianMudG9rZW5tYW5hZ2VyIiwiZXhwIjoxNDU5MTMwMjAxLCJuYmYiOjE0NTkxMjk5MDEsIm5vbmNlIjoiNzIyMTAwNTIwOTk3MjM4MiIsImlhdCI6MTQ1OTEyOTkwMSwiYXRfaGFzaCI6IkpnRFVDeW9hdEp5RW1HaWlXYndPaEEiLCJzaWQiOiIwYzVmMDYxZTYzOThiMWVjNmEwYmNlMmM5NDFlZTRjNSIsInN1YiI6Ijg4NDIxMTEzIiwiYXV0aF90aW1lIjoxNDU5MTI5ODk4LCJpZHAiOiJpZHNydiIsImFtciI6WyJwYXNzd29yZCJdfQ.f6S1Fdd0UQScZAFBzXwRiVsUIPQnWZLSe07kdtjANRZDZXf5A7yDtxOftgCx5W0ONQcDFVpLGPgTdhp7agZkPpCFutzmwr0Rr9G7E7mUN4xcIgAABhmRDfzDayFBEu6VM8wEWTChezSWtx2xG_2zmVJxxmNV0jvkaz0bu7iin-C_UZg6T-aI9FZDoKRGXZP9gF65FQ5pQ4bCYQxhKcvjjUfs0xSHGboL7waN6RfDpO4vvVR1Kz-PQhIRyFAJYRuoH4PdMczHYtFCb-k94r-7TxEU0vp61ww4WntbPvVWwUbCUgsEtmDzAZT-NEJVhWztNk1ip9wDPXzZ2hEhDAPJ7A"; - describe("validateJwt", function () { + jwtFromRsa = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMzMy9jb3JlIiwiYXVkIjoianMudG9rZW5tYW5hZ2VyIiwiZXhwIjoxNDU5MTMwMjAxLCJuYmYiOjE0NTkxMjk5MDEsIm5vbmNlIjoiNzIyMTAwNTIwOTk3MjM4MiIsImlhdCI6MTQ1OTEyOTkwMSwiYXRfaGFzaCI6IkpnRFVDeW9hdEp5RW1HaWlXYndPaEEiLCJzaWQiOiIwYzVmMDYxZTYzOThiMWVjNmEwYmNlMmM5NDFlZTRjNSIsInN1YiI6Ijg4NDIxMTEzIiwiYXV0aF90aW1lIjoxNDU5MTI5ODk4LCJpZHAiOiJpZHNydiIsImFtciI6WyJwYXNzd29yZCJdfQ.f6S1Fdd0UQScZAFBzXwRiVsUIPQnWZLSe07kdtjANRZDZXf5A7yDtxOftgCx5W0ONQcDFVpLGPgTdhp7agZkPpCFutzmwr0Rr9G7E7mUN4xcIgAABhmRDfzDayFBEu6VM8wEWTChezSWtx2xG_2zmVJxxmNV0jvkaz0bu7iin-C_UZg6T-aI9FZDoKRGXZP9gF65FQ5pQ4bCYQxhKcvjjUfs0xSHGboL7waN6RfDpO4vvVR1Kz-PQhIRyFAJYRuoH4PdMczHYtFCb-k94r-7TxEU0vp61ww4WntbPvVWwUbCUgsEtmDzAZT-NEJVhWztNk1ip9wDPXzZ2hEhDAPJ7A"; - it("should validate from RSA X509 key", function (done, fail) { - Log.level = Log.DEBUG; - delete rsaKey.n; - delete rsaKey.e; + rsaKey = { + kty: "RSA", + use: "sig", + kid: "a3rMUgMFv9tPclLa6yF3zAkfquE", + x5t: "a3rMUgMFv9tPclLa6yF3zAkfquE", + e: "AQAB", + n: "qnTksBdxOiOlsmRNd-mMS2M3o1IDpK4uAr0T4_YqO3zYHAGAWTwsq4ms-NWynqY5HaB4EThNxuq2GWC5JKpO1YirOrwS97B5x9LJyHXPsdJcSikEI9BxOkl6WLQ0UzPxHdYTLpR4_O-0ILAlXw8NU4-jB4AP8Sn9YGYJ5w0fLw5YmWioXeWvocz1wHrZdJPxS8XnqHXwMUozVzQj-x6daOv5FmrHU1r9_bbp0a1GLv4BbTtSh4kMyz1hXylho0EvPg5p9YIKStbNAW9eNWvv5R8HN7PPei21AsUqxekK0oW9jnEdHewckToX7x5zULWKwwZIksll0XnVczVgy7fCFw", + x5c: [ + "MIIDBTCCAfGgAwIBAgIQNQb+T2ncIrNA6cKvUA1GWTAJBgUrDgMCHQUAMBIxEDAOBgNVBAMTB0RldlJvb3QwHhcNMTAwMTIwMjIwMDAwWhcNMjAwMTIwMjIwMDAwWjAVMRMwEQYDVQQDEwppZHNydjN0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqnTksBdxOiOlsmRNd+mMS2M3o1IDpK4uAr0T4/YqO3zYHAGAWTwsq4ms+NWynqY5HaB4EThNxuq2GWC5JKpO1YirOrwS97B5x9LJyHXPsdJcSikEI9BxOkl6WLQ0UzPxHdYTLpR4/O+0ILAlXw8NU4+jB4AP8Sn9YGYJ5w0fLw5YmWioXeWvocz1wHrZdJPxS8XnqHXwMUozVzQj+x6daOv5FmrHU1r9/bbp0a1GLv4BbTtSh4kMyz1hXylho0EvPg5p9YIKStbNAW9eNWvv5R8HN7PPei21AsUqxekK0oW9jnEdHewckToX7x5zULWKwwZIksll0XnVczVgy7fCFwIDAQABo1wwWjATBgNVHSUEDDAKBggrBgEFBQcDATBDBgNVHQEEPDA6gBDSFgDaV+Q2d2191r6A38tBoRQwEjEQMA4GA1UEAxMHRGV2Um9vdIIQLFk7exPNg41NRNaeNu0I9jAJBgUrDgMCHQUAA4IBAQBUnMSZxY5xosMEW6Mz4WEAjNoNv2QvqNmk23RMZGMgr516ROeWS5D3RlTNyU8FkstNCC4maDM3E0Bi4bbzW3AwrpbluqtcyMN3Pivqdxx+zKWKiORJqqLIvN8CT1fVPxxXb/e9GOdaR8eXSmB0PgNUhM4IjgNkwBbvWC9F/lzvwjlQgciR7d4GfXPYsE1vf8tmdQaY8/PtdAkExmbrb9MihdggSoGXlELrPA91Yce+fiRcKY3rQlNWVd4DOoJ/cPXsXwry8pWjNCo5JD8Q+RQ5yZEy7YPoifwemLhTdsBz3hlZr28oCGJ3kbnpW0xGvQb3VHSTVVbeei0CfXoW6iz1" + ] + }; + + ecKey = { + kty: "EC", + kid: "4", + use: "sig", + alg: "EC", + crv: "P-256", + x: "eZXWiRe0I3TvHPXiGnvO944gjF1o4UmitH2CVwYIrPg", + y: "AKFNss7S35tOsp5iY7-YuLGs2cLrTKFk80JvgVzMPHQ3", + x5c: [ + "MIIBpDCCAUoCgYBCs6x21IvwVHFgJxiRegyHdSiEHFur9Wj2qM5oNkv6sFbbC75L849qCgMEzdtqIhCiCnFg6PaQdswHkcclXix+y0sycyIM6l429faY3jz5eQs5SYwXYkENStzTZBsWK6u7bPiV3HvjnIv+r1af8nvO5F0tiH0TC+auDj9FgRmYljAKBggqhkjOPQQDAjAeMRwwGgYDVQQDExNUZXN0IENBIENlcnRpZmljYXRlMB4XDTEzMDIxMTIxMjQxMVoXDTE0MDIxMTIxMjQxMVowHjEcMBoGA1UEAxMTVGVzdCBDQSBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHmV1okXtCN07xz14hp7zveOIIxdaOFJorR9glcGCKz4oU2yztLfm06ynmJjv5i4sazZwutMoWTzQm+BXMw8dDcwCgYIKoZIzj0EAwIDSAAwRQIhAI4aRAoTVm3was6UimA1lFL2RId+t/fExaviosXNKg/IAiBpZB4XXcnQISwauSJ1hXNnSEcONXdqvO5gDHu+X7QHLg==" + ] + }; + }); + + describe("parseJwt", function () { + + it("should parse a jwt", function () { + + var result = JoseUtil.parseJwt(jwt); + result.should.be.ok; + result.header.should.be.ok; + result.payload.should.be.ok; + + result.header.should.deep.equal({ + "typ": "JWT", + "alg": "RS256", + "x5t": "a3rMUgMFv9tPclLa6yF3zAkfquE", + "kid": "a3rMUgMFv9tPclLa6yF3zAkfquE" + }); + + result.payload.should.deep.equal({ + "iss": "https://localhost:44333/core", + "aud": "js.tokenmanager", + "exp": 1459130201, + "nbf": 1459129901, + "nonce": "7221005209972382", + "iat": 1459129901, + "at_hash": "JgDUCyoatJyEmGiiWbwOhA", + "sid": "0c5f061e6398b1ec6a0bce2c941ee4c5", + "sub": "88421113", + "auth_time": 1459129898, + "idp": "idsrv", + "amr": [ + "password" + ] + }); - JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expectedNow).then(()=>{ - done(); + }); + + it("should return undefined for an invalid jwt", function () { + + var result = JoseUtil.parseJwt("junk"); + expect(result).to.be.undefined; }); }); - it("should validate from RSA exponent and modulus", function (done) { - delete rsaKey.x5c; + describe("validateJwt", function () { - JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expectedNow).then(()=>{ - done(); - }) + if(!isRSA) { + it("should validate from RSA X509 key (jsrsasign only)", function (done, fail) { + Log.level = Log.DEBUG; + delete rsaKey.n; + delete rsaKey.e; - }); + JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expectedNow).then(()=>{ + done(); + }); - it("should fail for unsupported key types", function (done) { + }); + } - rsaKey.kty = "foo"; + it("should validate from RSA exponent and modulus", function (done) { + + delete rsaKey.x5c; + + JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expectedNow).then(()=>{ + done(); + }) - JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expectedNow).catch(e => { - e.message.should.contain("foo"); - done(); }); - }); + it("should fail for unsupported key types", function (done) { + + rsaKey.kty = "foo"; - it("should fail for mismatched keys", function (done) { + JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expectedNow).catch(e => { + e.message.should.contain("foo"); + done(); + }); - JoseUtil.validateJwt(jwtFromRsa, ecKey, expectedIssuer, expectedAudience, 0, expectedNow).catch(e => { - e.message.should.contain("signature"); - done(); }); - }); + it("should fail for mismatched keys", function (done) { - it("should not validate before nbf", function (done) { + JoseUtil.validateJwt(jwtFromRsa, ecKey, expectedIssuer, expectedAudience, 0, expectedNow).catch(e => { + e.message.should.contain("signature"); + done(); + }); - JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, notBefore - 1).catch(e => { - e.message.should.contain("iat"); - done(); }); - }); + it("should not validate before nbf", function (done) { - it("should allow nbf within clock skew", function (done) { + JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, notBefore - 1).catch(e => { + e.message.should.contain("iat"); + done(); + }); - var p1 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, notBefore - 1); - var p2 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, notBefore - 10); - Promise.all([p1, p2]).then(()=>{ - done(); }); - }); - it("should now allow nbf outside clock skew", function (done) { + it("should allow nbf within clock skew", function (done) { - JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, notBefore - 11).catch(e => { - e.message.should.contain("iat"); - done(); + var p1 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, notBefore - 1); + var p2 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, notBefore - 10); + Promise.all([p1, p2]).then(()=>{ + done(); + }); }); - }); + it("should now allow nbf outside clock skew", function (done) { - it("should not validate before iat", function (done) { + JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, notBefore - 11).catch(e => { + e.message.should.contain("iat"); + done(); + }); - JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, issuedAt - 1).catch(e => { - e.message.should.contain("iat"); - done(); }); - }); + it("should not validate before iat", function (done) { - it("should allow iat within clock skew", function (done) { + JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, issuedAt - 1).catch(e => { + e.message.should.contain("iat"); + done(); + }); - var p1 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, issuedAt - 1); - var p2 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, issuedAt - 10); - Promise.all([p1, p2]).then(()=>{ - done(); }); - }); - it("should now allow iat outside clock skew", function (done) { + it("should allow iat within clock skew", function (done) { - JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, issuedAt - 11).catch(e => { - e.message.should.contain("iat"); - done(); + var p1 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, issuedAt - 1); + var p2 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, issuedAt - 10); + Promise.all([p1, p2]).then(()=>{ + done(); + }); }); - }); + it("should now allow iat outside clock skew", function (done) { - it("should not validate after exp", function (done) { + JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, issuedAt - 11).catch(e => { + e.message.should.contain("iat"); + done(); + }); - JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expires + 1).catch(e => { - e.message.should.contain("exp"); - done(); }); - }); + it("should not validate after exp", function (done) { - it("should allow exp within clock skew", function (done) { + JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 0, expires + 1).catch(e => { + e.message.should.contain("exp"); + done(); + }); - var p1 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, expires + 1); - var p2 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, expires + 10) - Promise.all([p1, p2]).then(()=>{ - done(); }); - }); - it("should now allow exp outside clock skew", function (done) { + it("should allow exp within clock skew", function (done) { - JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, expires + 11).catch(e => { - e.message.should.contain("exp"); - done(); + var p1 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, expires + 1); + var p2 = JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, expires + 10) + Promise.all([p1, p2]).then(()=>{ + done(); + }); }); - }); + it("should now allow exp outside clock skew", function (done) { - it("should not validate for invalid audience", function (done) { + JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, expectedAudience, 10, expires + 11).catch(e => { + e.message.should.contain("exp"); + done(); + }); - JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, "invalid aud", 0, expectedNow).catch(e => { - e.message.should.contain("aud"); - done(); }); - }); - it("should not validate for invalid issuer", function (done) { + it("should not validate for invalid audience", function (done) { - JoseUtil.validateJwt(jwtFromRsa, rsaKey, "invalid issuer", expectedAudience, 0, expectedNow).catch(e => { - e.message.should.contain("issuer"); - done(); + JoseUtil.validateJwt(jwtFromRsa, rsaKey, expectedIssuer, "invalid aud", 0, expectedNow).catch(e => { + e.message.should.contain("aud"); + done(); + }); }); - }); + it("should not validate for invalid issuer", function (done) { + + JoseUtil.validateJwt(jwtFromRsa, rsaKey, "invalid issuer", expectedAudience, 0, expectedNow).catch(e => { + e.message.should.contain("issuer"); + done(); + }); + }); - }); + }); + + }); }); + diff --git a/webpack.base.js b/webpack.base.js index 38ac195b..2f7740ff 100644 --- a/webpack.base.js +++ b/webpack.base.js @@ -1,5 +1,5 @@ // create a webpack configuration with all common parts included here -var createWebpackConfig = function(options) { +function createWebpackConfig(options) { return { mode: options.mode, entry: options.entry, @@ -23,6 +23,6 @@ var createWebpackConfig = function(options) { devtool: options.devtool, optimization: options.optimization }; -}; +} module.exports = createWebpackConfig;