Skip to content

Commit

Permalink
Merge pull request #268 from auth0/temp-idtv-2
Browse files Browse the repository at this point in the history
Refactor RSA verification: Replace jsrsasign with crypto-js
  • Loading branch information
lbalmaceda committed Jan 30, 2020
2 parents b061c42 + 89e847b commit 72d8cd9
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 71 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
31 changes: 31 additions & 0 deletions src/jwt/__tests__/base64.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as base64 from '../base64';

describe('helpers base64 url', function() {
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=');
});
});

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',
);
});
});
});
12 changes: 12 additions & 0 deletions src/jwt/__tests__/jwks.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
97 changes: 39 additions & 58 deletions src/jwt/__tests__/jwt.spec.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();
});
Expand Down Expand Up @@ -70,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});
Expand All @@ -86,14 +75,31 @@ 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 jwks = getExpectedJwks();
jwks.keys[0].n = 'bad-modulus';

setupFetchMock({jwks});

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 () => {
const testJwt =
'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQifQ.eyJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJhdWQiOlsidG9rZW5zLXRlc3QtMTIzIiwiZXh0ZXJuYWwtdGVzdC0xMjMiXSwiZXhwIjoxNTcwMjAzMjgxLCJpYXQiOjE1NzAwMzA0ODEsIm5vbmNlIjoiYTU5dms1OTIiLCJhenAiOiJ0b2tlbnMtdGVzdC0xMjMiLCJhdXRoX3RpbWUiOjE1NzAxMTY4ODAuNjk0fQ.invalid-signature';

setupFetchMock();

await expect(verify(testJwt)).rejects.toHaveProperty(
'name',
'a0.idtoken.invalid_signature',
Expand All @@ -114,53 +120,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'),
'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});
setupFetchMock();

await expect(verify(testJwt)).resolves.toBeUndefined();
});

const setupFetchMock = ({
domain = BASE_EXPECTATIONS.domain,
jwks = getJwks(),
} = {}) => {
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 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',
},
],
};
};
});

describe('token claims verification', () => {
Expand Down Expand Up @@ -639,4 +603,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'),
);
};
});
32 changes: 32 additions & 0 deletions src/jwt/base64.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* 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) {
const paddingLength = 4;
const mod = str.length % paddingLength;
const pad = paddingLength - mod;

if (mod === 0) {
return str;
}

return str + new Array(1 + pad).join('=');
}

function byteArrayToHex(raw) {
let HEX = '';

for (let i = 0; i < raw.length; i++) {
const _hex = raw[i].toString(16);
HEX += _hex.length === 2 ? _hex : '0' + _hex;
}

return HEX;
}

export function decodeToHEX(str) {
return byteArrayToHex(base64.toByteArray(padding(str)));
}
72 changes: 72 additions & 0 deletions src/jwt/rsa-verifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
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';

const digestInfoHead = {
sha256: '3031300d060960864801650304020105000420',
};

const digestAlgs = {
sha256: SHA256,
};

function RSAVerifier(modulus, exp) {
this.n = null;
this.e = 0;

if (modulus && modulus.length > 0 && exp && 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 (let algName in digestInfoHead) {
const head = digestInfoHead[algName];
const len = head.length;

if (hDigestInfo.substring(0, len) === head) {
return {
alg: algName,
hash: hDigestInfo.substring(len),
};
}
}
return [];
}

RSAVerifier.prototype.verify = function(msg, encodedSignature) {
const decodedSignature = encodedSignature.replace(/[^0-9a-f]|[\s\n]]/gi, '');

const signature = new BigInteger(decodedSignature, 16);
if (signature.bitLength() > this.n.bitLength()) {
//Signature does not match with the key modulus.
return false;
}

const decryptedSignature = signature.modPowInt(this.e, this.n);
const digest = decryptedSignature.toString(16).replace(/^1f+00/, '');

const digestInfo = getAlgorithmFromDigest(digest);
if (digestInfo.length === 0) {
//Hashing algorithm is not found
return false;
}

if (!digestAlgs.hasOwnProperty(digestInfo.alg)) {
//Hashing algorithm is not supported
return false;
}

const msgHash = digestAlgs[digestInfo.alg](msg).toString();
return digestInfo.hash === msgHash;
};

export default RSAVerifier;
21 changes: 14 additions & 7 deletions src/jwt/signatureVerifier.js
Original file line number Diff line number Diff line change
@@ -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'];
Expand Down Expand Up @@ -45,22 +46,28 @@ export const verifySignature = (idToken, options) => {
}

return getJwk(options.domain, header.kid).then(jwk => {
const pubKey = KEYUTIL.getKey(jwk);
const signatureValid = KJUR.jws.JWS.verify(idToken, pubKey, ['RS256']);

if (signatureValid) {
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);
}

return Promise.reject(
idTokenError({
error: 'invalid_signature',
desc: 'Invalid token signature',
desc: 'Invalid ID token signature',
}),
);
});
};

const rsaVerifierForKey = jwk => {
const modulus = base64.decodeToHEX(jwk.n);
const exponent = base64.decodeToHEX(jwk.e);
return new RSAVerifier(modulus, exponent);
};

const getJwk = (domain, kid) => {
return getJwksUri(domain)
.then(uri => fetchJson(uri))
Expand Down
15 changes: 10 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 72d8cd9

Please sign in to comment.