From d20ae98d172b3e52aa2e9807718a8cfda4e80080 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 16 Jan 2020 17:39:45 -0300 Subject: [PATCH 01/11] replace jsrsasign with crypto-js --- package.json | 3 +- src/jwt/base64.js | 31 +++++++++++++++ src/jwt/rsa-verifier.js | 76 ++++++++++++++++++++++++++++++++++++ src/jwt/signatureVerifier.js | 38 +++++++++++------- yarn.lock | 15 ++++--- 5 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 src/jwt/base64.js create mode 100644 src/jwt/rsa-verifier.js diff --git a/package.json b/package.json index 1701f29f..09995e4b 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ }, "dependencies": { "base-64": "^0.1.0", - "jsrsasign": "8.0.12", + "crypto-js": "^3.1.9-1", + "jsbn": "^1.1.0", "jwt-decode": "^2.2.0", "url": "^0.11.0" }, diff --git a/src/jwt/base64.js b/src/jwt/base64.js new file mode 100644 index 00000000..e42bee7f --- /dev/null +++ b/src/jwt/base64.js @@ -0,0 +1,31 @@ +/** + * Borrowed from IDToken-verifier package + * https://github.com/auth0/idtoken-verifier/blob/master/src/helpers/base64.js + */ +import base64 from 'base64-js'; + +export function padding(str) { + var mod = str.length % 4; + var pad = 4 - mod; + + if (mod === 0) { + return str; + } + + return str + new Array(1 + pad).join('='); +} + +function 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; +} + +export function decodeToHEX(str) { + return byteArrayToHex(base64.toByteArray(padding(str))); +} diff --git a/src/jwt/rsa-verifier.js b/src/jwt/rsa-verifier.js new file mode 100644 index 00000000..be65f97c --- /dev/null +++ b/src/jwt/rsa-verifier.js @@ -0,0 +1,76 @@ +/* +Based on the work of Tom Wu +http://www-cs-students.stanford.edu/~tjw/jsbn/ +http://www-cs-students.stanford.edu/~tjw/jsbn/LICENSE +*/ + +import {BigInteger} from 'jsbn'; +import SHA256 from 'crypto-js/sha256'; + +var DigestInfoHead = { + sha1: '3021300906052b0e03021a05000414', + sha224: '302d300d06096086480165030402040500041c', + sha256: '3031300d060960864801650304020105000420', + sha384: '3041300d060960864801650304020205000430', + sha512: '3051300d060960864801650304020305000440', + md2: '3020300c06082a864886f70d020205000410', + md5: '3020300c06082a864886f70d020505000410', + ripemd160: '3021300906052b2403020105000414', +}; + +var DigestAlgs = { + 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 = encsig.replace(/[^0-9a-f]|[\s\n]]/gi, ''); + + 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; +}; + +export default RSAVerifier; diff --git a/src/jwt/signatureVerifier.js b/src/jwt/signatureVerifier.js index df653af3..61bc9d46 100644 --- a/src/jwt/signatureVerifier.js +++ b/src/jwt/signatureVerifier.js @@ -1,5 +1,6 @@ import AuthError from '../auth/authError'; -import {KEYUTIL, KJUR} from 'jsrsasign'; +import RSAVerifier from './rsa-verifier'; +import * as base64 from './base64'; const jwtDecoder = require('jwt-decode'); const ALLOWED_ALGORITHMS = ['RS256', 'HS256']; @@ -44,21 +45,30 @@ export const verifySignature = (idToken, options) => { return Promise.resolve(payload); } - return getJwk(options.domain, header.kid).then(jwk => { - const pubKey = KEYUTIL.getKey(jwk); - const signatureValid = KJUR.jws.JWS.verify(idToken, pubKey, ['RS256']); + return getJwk(options.domain, header.kid) + .then(jwk => rsaVerifierForKey(jwk)) + .then(rsaVerifier => { + const encodedParts = idToken.split('.'); + const headerAndPayload = encodedParts[0] + '.' + encodedParts[1]; + const signature = base64.decodeToHEX(encodedParts[2]); - if (signatureValid) { - return Promise.resolve(payload); - } + if (rsaVerifier.verify(headerAndPayload, signature)) { + return Promise.resolve(payload); + } - return Promise.reject( - idTokenError({ - error: 'invalid_signature', - desc: 'Invalid token signature', - }), - ); - }); + return Promise.reject( + idTokenError({ + error: 'invalid_signature', + desc: 'Invalid ID token signature', + }), + ); + }); +}; + +const rsaVerifierForKey = jwk => { + const modulus = base64.decodeToHEX(jwk.n); + const exponent = base64.decodeToHEX(jwk.e); + return Promise.resolve(new RSAVerifier(modulus, exponent)); }; const getJwk = (domain, kid) => { diff --git a/yarn.lock b/yarn.lock index 53d2170a..8fecd794 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1952,6 +1952,11 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" +crypto-js@^3.1.9-1: + version "3.1.9-1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.9-1.tgz#fda19e761fc077e01ffbfdc6e9fdfc59e8806cd8" + integrity sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg= + cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" @@ -3621,6 +3626,11 @@ js2xmlparser@^4.0.0: dependencies: xmlcreate "^2.0.0" +jsbn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA= + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -3796,11 +3806,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsrsasign@8.0.12: - version "8.0.12" - resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-8.0.12.tgz#22abb9656d34a30b9530436720835e89c2e5c316" - integrity sha1-Iqu5ZW00owuVMENnIINeicLlwxY= - jwt-decode@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" From 08c5cb28d162f2af1af58f4c2e00d8e984b87dd5 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 16 Jan 2020 17:40:05 -0300 Subject: [PATCH 02/11] add signature check tests --- src/jwt/__tests__/base64.spec.js | 17 +++++++++++++++++ src/jwt/__tests__/jwks.json | 12 ++++++++++++ src/jwt/__tests__/jwt.spec.js | 26 ++------------------------ 3 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 src/jwt/__tests__/base64.spec.js create mode 100644 src/jwt/__tests__/jwks.json diff --git a/src/jwt/__tests__/base64.spec.js b/src/jwt/__tests__/base64.spec.js new file mode 100644 index 00000000..bbdbbc47 --- /dev/null +++ b/src/jwt/__tests__/base64.spec.js @@ -0,0 +1,17 @@ +import * as base64 from '../base64'; + +describe('helpers base64 url', function() { + it('padding', function() { + expect(base64.padding('')).toBe(''); + expect(base64.padding('a')).toBe('a==='); + expect(base64.padding('ab')).toBe('ab=='); + expect(base64.padding('abc')).toBe('abc='); + expect(base64.padding('abcd')).toBe('abcd'); + expect(base64.padding('abced')).toBe('abced==='); + expect(base64.padding(base64.padding('abc'))).toBe('abc='); + }); + + it('decode to hex', function() { + expect(base64.decodeToHEX('AQAB')).toBe('010001'); + }); +}); diff --git a/src/jwt/__tests__/jwks.json b/src/jwt/__tests__/jwks.json new file mode 100644 index 00000000..42e1df18 --- /dev/null +++ b/src/jwt/__tests__/jwks.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "use": "sig", + "alg": "RS256", + "kty": "RSA", + "e": "AQAB", + "kid": "1234", + "n": "uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-spi5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902v9w-Iplu1WyoB2aPfitxEhRN0Yw" + } + ] +} diff --git a/src/jwt/__tests__/jwt.spec.js b/src/jwt/__tests__/jwt.spec.js index ffe854af..3c38b0f6 100644 --- a/src/jwt/__tests__/jwt.spec.js +++ b/src/jwt/__tests__/jwt.spec.js @@ -1,7 +1,6 @@ import verifyToken from '../index'; import * as signatureVerifier from '../signatureVerifier'; const jwtDecoder = require('jwt-decode'); -import {KEYUTIL} from 'jsrsasign'; import * as fs from 'fs'; import * as path from 'path'; import fetchMock from 'fetch-mock'; @@ -12,16 +11,6 @@ describe('id token verification tests', () => { fetchMock.restore(); }); - it('uses fixed version of jsrsasign', () => { - // jsrsasign has not been updated recently; we want to verify that the dependency is pinned to 8.0.12 - const packageData = fs.readFileSync( - path.resolve(__dirname, '../../../package.json'), - ); - const packageJson = JSON.parse(packageData); - const jsrsasignDepVersion = packageJson.dependencies.jsrsasign; - expect(jsrsasignDepVersion).toBe('8.0.12'); - }); - it('resolves when no idToken present', async () => { await expect(verify(undefined)).resolves.toBeUndefined(); }); @@ -114,22 +103,11 @@ describe('id token verification tests', () => { it('passes verification with valid token signed with RS256', async () => { const testJwt = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC0xMjMiXSwiZXhwIjoxNTY3NDg2ODAwLCJpYXQiOjE1NjczMTQwMDAsIm5vbmNlIjoiYTU5dms1OTIiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1NjczMTQwMDB9.ObH7oG3NsGaxWnB8rzbLOgAD2I0fr9dyZC81YUrbju3RwC3lRAxqJkbesiSdGKry9OamIhKYwUGpPK0wrBaRJo8UjDjICkhM6lGP23plysemxhDnFK1qjj-NaUaW1yKg14v2lVpQl7glW9LIhFDhpqIf4bILA2wt9-z8Uvi31ETZvGb8PDY2bEvjXR-69-yLuoTNT2skP9loKfz6hHDMQCTWrGA61BMMjkZBLo9UotD9BzN8V7bLrFFT25v6q9N83mWaGLsHntzPIl3EYPOwX0NbE0lXKar59TUqtaTB3uNFHbGjIYi8wuuIp4PV9arpE3YrjWOOmrMurD1KpIyQrQ'; - const contents = fs.readFileSync( - path.resolve(__dirname, './pubkey.pem'), + const jwks = fs.readFileSync( + path.resolve(__dirname, './jwks.json'), 'utf8', ); - const pubKey = KEYUTIL.getKey(contents); - const jwkFromKey = KEYUTIL.getJWKFromKey(pubKey); - - jwkFromKey.kid = '1234'; - jwkFromKey.alg = 'RS256'; - jwkFromKey.use = 'sig'; - - const jwks = { - keys: [jwkFromKey], - }; - setupFetchMock({jwks}); await expect(verify(testJwt)).resolves.toBeUndefined(); From 5c89e6b0c99ba3e36a536d2381160f351c67a465 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 16 Jan 2020 18:34:40 -0300 Subject: [PATCH 03/11] add more signature tests --- src/jwt/__tests__/jwt.spec.js | 46 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/jwt/__tests__/jwt.spec.js b/src/jwt/__tests__/jwt.spec.js index 3c38b0f6..949506f0 100644 --- a/src/jwt/__tests__/jwt.spec.js +++ b/src/jwt/__tests__/jwt.spec.js @@ -75,13 +75,25 @@ describe('id token verification tests', () => { ); }); - it('fails when signature is not verified', async () => { + it('fails when public key is invalid and cannot be reconstructed', async () => { const testJwt = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC0xMjMiXSwiZXhwIjoxNTcwMjAzMjgxLCJpYXQiOjE1NzAwMzA0ODEsIm5vbmNlIjoiYTU5dms1OTIiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1NzAxMTY4ODAuNjk0fQ.ZNPsQq_U8NGyi5WFNgvuT0QlxfGFS9w6YIHWiF4dnwz_Zf3mv3gh4wybDR8vaLCE8ONTXvT9V_rW6oqNHSvEwa0nvPy2Vi3gVAvSfusoiYhkuQG_6SuqbeOrNJ1cejGzqw_iv2s6yEyN3B9wp0TCuIKL5jLPttaRi6ouGCbYeReANecaLOVZstrO4GhlY0NwtT4j5Dn1tDYavWxi1DZBisxBvMEFA6N0aQa51gJm6RYtUjBTo50j1xG5b7TIF4edjjT85FYQgrwEzA7Ss3HpnrYXEEvHn4nCsc585T3GKQuF21Nli-qGgQ3MywPOOqqiCSvL254Cp88Gt3xDS1hnqg'; - const jwks = getJwks(); - jwks.keys[0].n += 'bad'; + const corruptedJwks = getExpectedJwks(); + corruptedJwks.keys[0].n += 'bad-modulus'; - setupFetchMock({jwks}); + setupFetchMock({corruptedJwks}); + + await expect(verify(testJwt)).rejects.toHaveProperty( + 'name', + 'a0.idtoken.invalid_signature', + ); + }); + + it('fails when signature is invalid', async () => { + const testJwt = + 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC0xMjMiXSwiZXhwIjoxNTcwMjAzMjgxLCJpYXQiOjE1NzAwMzA0ODEsIm5vbmNlIjoiYTU5dms1OTIiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1NzAxMTY4ODAuNjk0fQ.invalid-signature'; + + setupFetchMock(); await expect(verify(testJwt)).rejects.toHaveProperty( 'name', @@ -103,19 +115,15 @@ describe('id token verification tests', () => { it('passes verification with valid token signed with RS256', async () => { const testJwt = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC0xMjMiXSwiZXhwIjoxNTY3NDg2ODAwLCJpYXQiOjE1NjczMTQwMDAsIm5vbmNlIjoiYTU5dms1OTIiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1NjczMTQwMDB9.ObH7oG3NsGaxWnB8rzbLOgAD2I0fr9dyZC81YUrbju3RwC3lRAxqJkbesiSdGKry9OamIhKYwUGpPK0wrBaRJo8UjDjICkhM6lGP23plysemxhDnFK1qjj-NaUaW1yKg14v2lVpQl7glW9LIhFDhpqIf4bILA2wt9-z8Uvi31ETZvGb8PDY2bEvjXR-69-yLuoTNT2skP9loKfz6hHDMQCTWrGA61BMMjkZBLo9UotD9BzN8V7bLrFFT25v6q9N83mWaGLsHntzPIl3EYPOwX0NbE0lXKar59TUqtaTB3uNFHbGjIYi8wuuIp4PV9arpE3YrjWOOmrMurD1KpIyQrQ'; - const jwks = fs.readFileSync( - path.resolve(__dirname, './jwks.json'), - 'utf8', - ); - setupFetchMock({jwks}); + setupFetchMock(); await expect(verify(testJwt)).resolves.toBeUndefined(); }); const setupFetchMock = ({ domain = BASE_EXPECTATIONS.domain, - jwks = getJwks(), + jwks = getExpectedJwks(), } = {}) => { const expectedDiscoveryUri = `https://${domain}/.well-known/openid-configuration`; const expectedJwksUri = `https://${domain}/.well-known/jwks.json`; @@ -124,20 +132,10 @@ describe('id token verification tests', () => { fetchMock.get(expectedJwksUri, jwks); }; - const getJwks = () => { - return { - keys: [ - { - kty: 'RSA', - n: - 'st69ml_DI8MhepFSV9o8zjzRFiEst1_1-XJe0ib-g_aMauGTFOqeITdVqWTJMzZsjtwsPFD1CXbmEtI282GBbniJ7XkrZwpjzXangbvJpFE-aBmKeogTq6B94a19H9umCtV7eC55xDmOylXYPFdcVFvolWajdYGywqH8d4Cu_pIB25ELoA78goP4MqweJhnOt4r5jORea2paLXa04ojvglbOGnFec65Y4Hyw2mWGu06f0sxW-LMGzwP_SgbpRDKKnn-W8grguPq63sLexDTBFLyPNCcFQ8wnEQzLCaNNJItu-OFwgwgJhiB3d0et5m3lF2_lEJ2Pwndp0ORlOWcJbQbad', - e: 'AQAB', - kid: '1234', - alg: 'RS256', - use: 'sig', - }, - ], - }; + const getExpectedJwks = () => { + return JSON.parse( + fs.readFileSync(path.resolve(__dirname, './jwks.json'), 'utf8'), + ); }; }); From 97b4543adda795b63789d8c2a0bc167e5d423d9f Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 17 Jan 2020 13:37:52 -0300 Subject: [PATCH 04/11] fix rebase issue --- src/jwt/__tests__/jwt.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jwt/__tests__/jwt.spec.js b/src/jwt/__tests__/jwt.spec.js index 949506f0..d1179336 100644 --- a/src/jwt/__tests__/jwt.spec.js +++ b/src/jwt/__tests__/jwt.spec.js @@ -59,7 +59,7 @@ describe('id token verification tests', () => { const testJwt = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC0xMjMiXSwiZXhwIjoxNTcwMjAyOTMxLCJpYXQiOjE1NzAwMzAxMzEsIm5vbmNlIjoiYTU5dms1OTIiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1NzAxMTY1MzAuNzk2fQ.Xad-J3PtImY3z--Gvj-H61tH18mCGQUUBkcug-CB5ehkjd56PXrA-AJHZK7OLryB_uj6sFKVn-V8Wr6t3KW7_Fd2n-__Ca2h6PtgIrjceZlHAQY4SgAk9tPmeeTOhs6KyXDeW0Ot0j3CP9p7nWxgCGMu_H5J5ZgJSVUVlffVpaIMEGiFZ_r71PLPtuTL3GsDwtICG_5xuqoR2YBLSpNuuc46t15i94E3JC1UXGryRfxVbeHg3x5DF9nf6eVkMHRdi-CdNQn2iD0G9OmxxELh-40pecbyUxLv4NfTHmbxOdvWRK00N8sgkElnPnoWXb5pacxLShFsBTJdXIsyqF_onA'; - const jwks = getJwks(); + const jwks = getExpectedJwks(); jwks.keys[0].kid = '4321'; setupFetchMock({jwks}); From ce822bce377bd94ba693fbe8065fca267832a9db Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 29 Jan 2020 18:42:02 -0300 Subject: [PATCH 05/11] move sync op outside of promise --- src/jwt/signatureVerifier.js | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/jwt/signatureVerifier.js b/src/jwt/signatureVerifier.js index 61bc9d46..fa360d5f 100644 --- a/src/jwt/signatureVerifier.js +++ b/src/jwt/signatureVerifier.js @@ -45,30 +45,29 @@ export const verifySignature = (idToken, options) => { return Promise.resolve(payload); } - return getJwk(options.domain, header.kid) - .then(jwk => rsaVerifierForKey(jwk)) - .then(rsaVerifier => { - const encodedParts = idToken.split('.'); - const headerAndPayload = encodedParts[0] + '.' + encodedParts[1]; - const signature = base64.decodeToHEX(encodedParts[2]); + return getJwk(options.domain, header.kid).then(jwk => { + const rsaVerifier = rsaVerifierForKey(jwk); + const encodedParts = idToken.split('.'); + const headerAndPayload = encodedParts[0] + '.' + encodedParts[1]; + const signature = base64.decodeToHEX(encodedParts[2]); - if (rsaVerifier.verify(headerAndPayload, signature)) { - return Promise.resolve(payload); - } + if (rsaVerifier.verify(headerAndPayload, signature)) { + return Promise.resolve(payload); + } - return Promise.reject( - idTokenError({ - error: 'invalid_signature', - desc: 'Invalid ID token signature', - }), - ); - }); + return Promise.reject( + idTokenError({ + error: 'invalid_signature', + desc: 'Invalid ID token signature', + }), + ); + }); }; const rsaVerifierForKey = jwk => { const modulus = base64.decodeToHEX(jwk.n); const exponent = base64.decodeToHEX(jwk.e); - return Promise.resolve(new RSAVerifier(modulus, exponent)); + return new RSAVerifier(modulus, exponent); }; const getJwk = (domain, kid) => { From de3c2e23287d261ee04f879c443f8ff62da080a0 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 29 Jan 2020 18:48:53 -0300 Subject: [PATCH 06/11] refactor: move test helper methods to bottom of file --- src/jwt/__tests__/jwt.spec.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/jwt/__tests__/jwt.spec.js b/src/jwt/__tests__/jwt.spec.js index d1179336..cb33ba32 100644 --- a/src/jwt/__tests__/jwt.spec.js +++ b/src/jwt/__tests__/jwt.spec.js @@ -120,23 +120,6 @@ describe('id token verification tests', () => { await expect(verify(testJwt)).resolves.toBeUndefined(); }); - - const setupFetchMock = ({ - domain = BASE_EXPECTATIONS.domain, - jwks = getExpectedJwks(), - } = {}) => { - const expectedDiscoveryUri = `https://${domain}/.well-known/openid-configuration`; - const expectedJwksUri = `https://${domain}/.well-known/jwks.json`; - - fetchMock.get(expectedDiscoveryUri, {jwks_uri: expectedJwksUri}); - fetchMock.get(expectedJwksUri, jwks); - }; - - const getExpectedJwks = () => { - return JSON.parse( - fs.readFileSync(path.resolve(__dirname, './jwks.json'), 'utf8'), - ); - }; }); describe('token claims verification', () => { @@ -615,4 +598,21 @@ describe('id token verification tests', () => { const options = Object.assign({}, optionsDefaults, optionsOverrides); return verifyToken(idToken, options); }; + + const setupFetchMock = ({ + domain = BASE_EXPECTATIONS.domain, + jwks = getExpectedJwks(), + } = {}) => { + const expectedDiscoveryUri = `https://${domain}/.well-known/openid-configuration`; + const expectedJwksUri = `https://${domain}/.well-known/jwks.json`; + + fetchMock.get(expectedDiscoveryUri, {jwks_uri: expectedJwksUri}); + fetchMock.get(expectedJwksUri, jwks); + }; + + const getExpectedJwks = () => { + return JSON.parse( + fs.readFileSync(path.resolve(__dirname, './jwks.json'), 'utf8'), + ); + }; }); From 5ae8bdbd5498199cb55e435a076c3ab09a26d121 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 29 Jan 2020 19:09:42 -0300 Subject: [PATCH 07/11] migrate to const and let --- src/jwt/base64.js | 11 ++++++----- src/jwt/rsa-verifier.js | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/jwt/base64.js b/src/jwt/base64.js index e42bee7f..0fb5488c 100644 --- a/src/jwt/base64.js +++ b/src/jwt/base64.js @@ -5,8 +5,9 @@ import base64 from 'base64-js'; export function padding(str) { - var mod = str.length % 4; - var pad = 4 - mod; + const paddingLength = 4; + const mod = str.length % paddingLength; + const pad = paddingLength - mod; if (mod === 0) { return str; @@ -16,10 +17,10 @@ export function padding(str) { } function byteArrayToHex(raw) { - var HEX = ''; + let HEX = ''; - for (var i = 0; i < raw.length; i++) { - var _hex = raw[i].toString(16); + for (let i = 0; i < raw.length; i++) { + const _hex = raw[i].toString(16); HEX += _hex.length === 2 ? _hex : '0' + _hex; } diff --git a/src/jwt/rsa-verifier.js b/src/jwt/rsa-verifier.js index be65f97c..899c58a8 100644 --- a/src/jwt/rsa-verifier.js +++ b/src/jwt/rsa-verifier.js @@ -7,7 +7,7 @@ http://www-cs-students.stanford.edu/~tjw/jsbn/LICENSE import {BigInteger} from 'jsbn'; import SHA256 from 'crypto-js/sha256'; -var DigestInfoHead = { +const digestInfoHead = { sha1: '3021300906052b0e03021a05000414', sha224: '302d300d06096086480165030402040500041c', sha256: '3031300d060960864801650304020105000420', @@ -18,7 +18,7 @@ var DigestInfoHead = { ripemd160: '3021300906052b2403020105000414', }; -var DigestAlgs = { +const digestAlgs = { sha256: SHA256, }; @@ -35,9 +35,9 @@ function RSAVerifier(modulus, exp) { } function getAlgorithmFromDigest(hDigestInfo) { - for (var algName in DigestInfoHead) { - var head = DigestInfoHead[algName]; - var len = head.length; + for (let algName in digestInfoHead) { + const head = digestInfoHead[algName]; + const len = head.length; if (hDigestInfo.substring(0, len) === head) { return { @@ -49,27 +49,27 @@ function getAlgorithmFromDigest(hDigestInfo) { return []; } -RSAVerifier.prototype.verify = function(msg, encsig) { - encsig = encsig.replace(/[^0-9a-f]|[\s\n]]/gi, ''); +RSAVerifier.prototype.verify = function(msg, encodedSignature) { + const decodedSignature = encodedSignature.replace(/[^0-9a-f]|[\s\n]]/gi, ''); - var sig = new BigInteger(encsig, 16); - if (sig.bitLength() > this.n.bitLength()) { + const signature = new BigInteger(decodedSignature, 16); + if (signature.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/, ''); + const decryptedSignature = signature.modPowInt(this.e, this.n); + const digest = decryptedSignature.toString(16).replace(/^1f+00/, ''); - var digestInfo = getAlgorithmFromDigest(digest); + const digestInfo = getAlgorithmFromDigest(digest); if (digestInfo.length === 0) { return false; } - if (!DigestAlgs.hasOwnProperty(digestInfo.alg)) { + if (!digestAlgs.hasOwnProperty(digestInfo.alg)) { throw new Error('Hashing algorithm is not supported.'); } - var msgHash = DigestAlgs[digestInfo.alg](msg).toString(); + const msgHash = digestAlgs[digestInfo.alg](msg).toString(); return digestInfo.hash === msgHash; }; From 20455d1a656b60d7bd6a0b55434355624914609f Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 30 Jan 2020 14:44:31 -0300 Subject: [PATCH 08/11] stop throwing rsa verifier errors --- src/jwt/__tests__/jwt.spec.js | 13 +++++++++---- src/jwt/rsa-verifier.js | 7 +++++-- src/jwt/signatureVerifier.js | 2 -- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/jwt/__tests__/jwt.spec.js b/src/jwt/__tests__/jwt.spec.js index cb33ba32..c06948fa 100644 --- a/src/jwt/__tests__/jwt.spec.js +++ b/src/jwt/__tests__/jwt.spec.js @@ -78,15 +78,20 @@ describe('id token verification tests', () => { it('fails when public key is invalid and cannot be reconstructed', async () => { const testJwt = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC0xMjMiXSwiZXhwIjoxNTcwMjAzMjgxLCJpYXQiOjE1NzAwMzA0ODEsIm5vbmNlIjoiYTU5dms1OTIiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1NzAxMTY4ODAuNjk0fQ.ZNPsQq_U8NGyi5WFNgvuT0QlxfGFS9w6YIHWiF4dnwz_Zf3mv3gh4wybDR8vaLCE8ONTXvT9V_rW6oqNHSvEwa0nvPy2Vi3gVAvSfusoiYhkuQG_6SuqbeOrNJ1cejGzqw_iv2s6yEyN3B9wp0TCuIKL5jLPttaRi6ouGCbYeReANecaLOVZstrO4GhlY0NwtT4j5Dn1tDYavWxi1DZBisxBvMEFA6N0aQa51gJm6RYtUjBTo50j1xG5b7TIF4edjjT85FYQgrwEzA7Ss3HpnrYXEEvHn4nCsc585T3GKQuF21Nli-qGgQ3MywPOOqqiCSvL254Cp88Gt3xDS1hnqg'; - const corruptedJwks = getExpectedJwks(); - corruptedJwks.keys[0].n += 'bad-modulus'; + const jwks = getExpectedJwks(); + jwks.keys[0].n = 'bad-modulus'; - setupFetchMock({corruptedJwks}); + setupFetchMock({jwks}); - await expect(verify(testJwt)).rejects.toHaveProperty( + const result = verify(testJwt); + expect(result).rejects.toHaveProperty( 'name', 'a0.idtoken.invalid_signature', ); + expect(result).rejects.toHaveProperty( + 'message', + 'Invalid ID token signature', + ); }); it('fails when signature is invalid', async () => { diff --git a/src/jwt/rsa-verifier.js b/src/jwt/rsa-verifier.js index 899c58a8..27522c69 100644 --- a/src/jwt/rsa-verifier.js +++ b/src/jwt/rsa-verifier.js @@ -54,7 +54,8 @@ RSAVerifier.prototype.verify = function(msg, encodedSignature) { const signature = new BigInteger(decodedSignature, 16); if (signature.bitLength() > this.n.bitLength()) { - throw new Error('Signature does not match with the key modulus.'); + //Signature does not match with the key modulus. + return false; } const decryptedSignature = signature.modPowInt(this.e, this.n); @@ -62,11 +63,13 @@ RSAVerifier.prototype.verify = function(msg, encodedSignature) { const digestInfo = getAlgorithmFromDigest(digest); if (digestInfo.length === 0) { + //Hashing algorithm is not found return false; } if (!digestAlgs.hasOwnProperty(digestInfo.alg)) { - throw new Error('Hashing algorithm is not supported.'); + //Hashing algorithm is not supported + return false; } const msgHash = digestAlgs[digestInfo.alg](msg).toString(); diff --git a/src/jwt/signatureVerifier.js b/src/jwt/signatureVerifier.js index fa360d5f..94b6e238 100644 --- a/src/jwt/signatureVerifier.js +++ b/src/jwt/signatureVerifier.js @@ -50,11 +50,9 @@ export const verifySignature = (idToken, options) => { const encodedParts = idToken.split('.'); const headerAndPayload = encodedParts[0] + '.' + encodedParts[1]; const signature = base64.decodeToHEX(encodedParts[2]); - if (rsaVerifier.verify(headerAndPayload, signature)) { return Promise.resolve(payload); } - return Promise.reject( idTokenError({ error: 'invalid_signature', From 98b8fe1166a6761c3dad0026fe69b1e3496490af Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 30 Jan 2020 14:51:03 -0300 Subject: [PATCH 09/11] improve truth value checks on rsa verifier file --- src/jwt/rsa-verifier.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jwt/rsa-verifier.js b/src/jwt/rsa-verifier.js index 27522c69..da009351 100644 --- a/src/jwt/rsa-verifier.js +++ b/src/jwt/rsa-verifier.js @@ -26,7 +26,7 @@ function RSAVerifier(modulus, exp) { this.n = null; this.e = 0; - if (modulus != null && exp != null && modulus.length > 0 && exp.length > 0) { + if (modulus && modulus.length > 0 && exp && exp.length > 0) { this.n = new BigInteger(modulus, 16); this.e = parseInt(exp, 16); } else { From 894b8dcb11d9e972bcc15cf531a0b0f1ed4bb361 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 30 Jan 2020 14:51:57 -0300 Subject: [PATCH 10/11] remove unused/unsupported hashing algorithms --- src/jwt/rsa-verifier.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/jwt/rsa-verifier.js b/src/jwt/rsa-verifier.js index da009351..3cb7303d 100644 --- a/src/jwt/rsa-verifier.js +++ b/src/jwt/rsa-verifier.js @@ -8,14 +8,7 @@ import {BigInteger} from 'jsbn'; import SHA256 from 'crypto-js/sha256'; const digestInfoHead = { - sha1: '3021300906052b0e03021a05000414', - sha224: '302d300d06096086480165030402040500041c', sha256: '3031300d060960864801650304020105000420', - sha384: '3041300d060960864801650304020205000430', - sha512: '3051300d060960864801650304020305000440', - md2: '3020300c06082a864886f70d020205000410', - md5: '3020300c06082a864886f70d020505000410', - ripemd160: '3021300906052b2403020105000414', }; const digestAlgs = { From 89e847b5a8bf0a82d6f7166d3bac0d6c582bcbc8 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 30 Jan 2020 15:01:03 -0300 Subject: [PATCH 11/11] refactor b64 tests --- src/jwt/__tests__/base64.spec.js | 34 ++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/jwt/__tests__/base64.spec.js b/src/jwt/__tests__/base64.spec.js index bbdbbc47..af409bef 100644 --- a/src/jwt/__tests__/base64.spec.js +++ b/src/jwt/__tests__/base64.spec.js @@ -1,17 +1,31 @@ import * as base64 from '../base64'; describe('helpers base64 url', function() { - it('padding', function() { - expect(base64.padding('')).toBe(''); - expect(base64.padding('a')).toBe('a==='); - expect(base64.padding('ab')).toBe('ab=='); - expect(base64.padding('abc')).toBe('abc='); - expect(base64.padding('abcd')).toBe('abcd'); - expect(base64.padding('abced')).toBe('abced==='); - expect(base64.padding(base64.padding('abc'))).toBe('abc='); + describe('padding', function() { + it('does not add to multiple of 4', function() { + expect(base64.padding('')).toBe(''); + expect(base64.padding('abcd')).toBe('abcd'); + }); + it('adds to non multiple of 4', function() { + expect(base64.padding('a')).toBe('a==='); + expect(base64.padding('ab')).toBe('ab=='); + expect(base64.padding('abc')).toBe('abc='); + expect(base64.padding('abced')).toBe('abced==='); + }); + it('does not change already padded value', function() { + const padded = base64.padding('abc'); + expect(padded).toBe('abc='); + const again = base64.padding(padded); + expect(again).toBe('abc='); + }); }); - it('decode to hex', function() { - expect(base64.decodeToHEX('AQAB')).toBe('010001'); + describe('decoding to hex', function() { + it('should convert base64 input into hex output', function() { + expect(base64.decodeToHEX('AQAB')).toBe('010001'); + expect(base64.decodeToHEX('uGbXWiK3dQTyCbX5')).toBe( + 'b866d75a22b77504f209b5f9', + ); + }); }); });