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

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.