use token and algo from jwt header (#6416)

* use token and algo from jwt header

* change node-rsa out for jwks-rsa, reflect change in tests and add one test for coverage

* remove superfluous cache, allow jwks cache parameters to be passed to validateAuthData

* remove package lock

* regenerate package lock

* try fixing package-lock with copy from master

* manual changes for merge conflict

* whitespace

* pass options as object

* fix inconsistent variable name
This commit is contained in:
Andy
2020-03-11 15:29:20 -05:00
committed by GitHub
parent 16a974a04a
commit 8e0e485de1
4 changed files with 272 additions and 218 deletions

293
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,12 +39,12 @@
"graphql-upload": "10.0.0",
"intersect": "1.0.1",
"jsonwebtoken": "8.5.1",
"jwks-rsa": "1.7.0",
"ldapjs": "1.0.2",
"lodash": "4.17.15",
"lru-cache": "5.1.1",
"mime": "2.4.4",
"mongodb": "3.5.4",
"node-rsa": "1.0.7",
"parse": "2.11.0",
"pg-promise": "10.4.4",
"pluralize": "^8.0.0",

View File

@@ -1136,21 +1136,83 @@ describe('oauth2 auth adapter', () => {
describe('apple signin auth adapter', () => {
const apple = require('../lib/Adapters/Auth/apple');
const jwt = require('jsonwebtoken');
const util = require('util');
it('should throw error with missing id_token', async () => {
try {
await apple.validateAuthData({}, { client_id: 'secret' });
await apple.validateAuthData({}, { clientId: 'secret' });
fail();
} catch (e) {
expect(e.message).toBe('id token is invalid for this user.');
}
});
it('should not verify invalid id_token', async () => {
it('should not decode invalid id_token', async () => {
try {
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ client_id: 'secret' }
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('provided token does not decode as JWT');
}
});
it('should throw error if public key used to encode token is not available', async () => {
const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
try {
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe(
`Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
);
}
});
it('should use algorithm from key header to verify id_token', async () => {
const fakeClaim = {
iss: 'https://appleid.apple.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
const result = await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
expect(result).toEqual(fakeClaim);
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(
fakeDecodedToken.header.alg
);
});
it('should not verify invalid id_token', async () => {
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
try {
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
@@ -1165,11 +1227,17 @@ describe('apple signin auth adapter', () => {
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const result = await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ client_id: 'secret' }
{ clientId: 'secret' }
);
expect(result).toEqual(fakeClaim);
});
@@ -1179,12 +1247,18 @@ describe('apple signin auth adapter', () => {
iss: 'https://not.apple.com',
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ client_id: 'secret' }
{ clientId: 'secret' }
);
fail();
} catch (e) {
@@ -1200,12 +1274,18 @@ describe('apple signin auth adapter', () => {
aud: 'invalid_client_id',
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ client_id: 'secret' }
{ clientId: 'secret' }
);
fail();
} catch (e) {
@@ -1214,8 +1294,32 @@ describe('apple signin auth adapter', () => {
);
}
});
});
it('should throw error with with invalid user id', async () => {
const fakeClaim = {
iss: 'https://appleid.apple.com',
aud: 'invalid_client_id',
sub: 'a_different_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('auth data is invalid for this user.');
}
});
});
describe('Apple Game Center Auth adapter', () => {
const gcenter = require('../lib/Adapters/Auth/gcenter');

View File

@@ -2,45 +2,68 @@
// https://developer.apple.com/documentation/signinwithapplerestapi
const Parse = require('parse/node').Parse;
const httpsRequest = require('./httpsRequest');
const NodeRSA = require('node-rsa');
const jwksClient = require('jwks-rsa');
const util = require('util');
const jwt = require('jsonwebtoken');
const TOKEN_ISSUER = 'https://appleid.apple.com';
let currentKey;
const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
const client = jwksClient({
jwksUri: `${TOKEN_ISSUER}/auth/keys`,
cache: true,
cacheMaxEntries,
cacheMaxAge,
});
const getApplePublicKey = async () => {
let data;
const asyncGetSigningKeyFunction = util.promisify(client.getSigningKey);
let key;
try {
data = await httpsRequest.get('https://appleid.apple.com/auth/keys');
} catch (e) {
if (currentKey) {
return currentKey;
}
throw e;
key = await asyncGetSigningKeyFunction(keyId);
} catch (error) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Unable to find matching key for Key ID: ${keyId}`
);
}
const key = data.keys[0];
const pubKey = new NodeRSA();
pubKey.importKey(
{ n: Buffer.from(key.n, 'base64'), e: Buffer.from(key.e, 'base64') },
'components-public'
);
currentKey = pubKey.exportKey(['public']);
return currentKey;
return key;
};
const verifyIdToken = async ({ token, id }, clientID) => {
const getHeaderFromToken = token => {
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
throw Error('provided token does not decode as JWT');
}
return decodedToken.header;
};
const verifyIdToken = async (
{ token, id },
{ clientId, cacheMaxEntries, cacheMaxAge }
) => {
if (!token) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'id token is invalid for this user.'
);
}
const applePublicKey = await getApplePublicKey();
const jwtClaims = jwt.verify(token, applePublicKey, { algorithms: 'RS256' });
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
const ONE_HOUR_IN_MS = 3600000;
cacheMaxAge = cacheMaxAge || ONE_HOUR_IN_MS;
cacheMaxEntries = cacheMaxEntries || 5;
const appleKey = await getAppleKeyByKeyId(
keyId,
cacheMaxEntries,
cacheMaxAge
);
const signingKey = appleKey.publicKey || appleKey.rsaPublicKey;
const jwtClaims = jwt.verify(token, signingKey, {
algorithms: algorithm,
});
if (jwtClaims.iss !== TOKEN_ISSUER) {
throw new Parse.Error(
@@ -54,10 +77,10 @@ const verifyIdToken = async ({ token, id }, clientID) => {
`auth data is invalid for this user.`
);
}
if (clientID !== undefined && jwtClaims.aud !== clientID) {
if (clientId !== undefined && jwtClaims.aud !== clientId) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`jwt aud parameter does not include this client - is: ${jwtClaims.aud} | expected: ${clientID}`
`jwt aud parameter does not include this client - is: ${jwtClaims.aud} | expected: ${clientId}`
);
}
return jwtClaims;
@@ -65,7 +88,7 @@ const verifyIdToken = async ({ token, id }, clientID) => {
// Returns a promise that fulfills if this id token is valid
function validateAuthData(authData, options = {}) {
return verifyIdToken(authData, options.client_id);
return verifyIdToken(authData, options);
}
// Returns a promise that fulfills if this app id is valid.