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:
293
package-lock.json
generated
293
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -39,12 +39,12 @@
|
|||||||
"graphql-upload": "10.0.0",
|
"graphql-upload": "10.0.0",
|
||||||
"intersect": "1.0.1",
|
"intersect": "1.0.1",
|
||||||
"jsonwebtoken": "8.5.1",
|
"jsonwebtoken": "8.5.1",
|
||||||
|
"jwks-rsa": "1.7.0",
|
||||||
"ldapjs": "1.0.2",
|
"ldapjs": "1.0.2",
|
||||||
"lodash": "4.17.15",
|
"lodash": "4.17.15",
|
||||||
"lru-cache": "5.1.1",
|
"lru-cache": "5.1.1",
|
||||||
"mime": "2.4.4",
|
"mime": "2.4.4",
|
||||||
"mongodb": "3.5.4",
|
"mongodb": "3.5.4",
|
||||||
"node-rsa": "1.0.7",
|
|
||||||
"parse": "2.11.0",
|
"parse": "2.11.0",
|
||||||
"pg-promise": "10.4.4",
|
"pg-promise": "10.4.4",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
|
|||||||
@@ -1136,21 +1136,83 @@ describe('oauth2 auth adapter', () => {
|
|||||||
describe('apple signin auth adapter', () => {
|
describe('apple signin auth adapter', () => {
|
||||||
const apple = require('../lib/Adapters/Auth/apple');
|
const apple = require('../lib/Adapters/Auth/apple');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
it('should throw error with missing id_token', async () => {
|
it('should throw error with missing id_token', async () => {
|
||||||
try {
|
try {
|
||||||
await apple.validateAuthData({}, { client_id: 'secret' });
|
await apple.validateAuthData({}, { clientId: 'secret' });
|
||||||
fail();
|
fail();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe('id token is invalid for this user.');
|
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 {
|
try {
|
||||||
await apple.validateAuthData(
|
await apple.validateAuthData(
|
||||||
{ id: 'the_user_id', token: 'the_token' },
|
{ 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();
|
fail();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1165,11 +1227,17 @@ describe('apple signin auth adapter', () => {
|
|||||||
exp: Date.now(),
|
exp: Date.now(),
|
||||||
sub: 'the_user_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);
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
const result = await apple.validateAuthData(
|
const result = await apple.validateAuthData(
|
||||||
{ id: 'the_user_id', token: 'the_token' },
|
{ id: 'the_user_id', token: 'the_token' },
|
||||||
{ client_id: 'secret' }
|
{ clientId: 'secret' }
|
||||||
);
|
);
|
||||||
expect(result).toEqual(fakeClaim);
|
expect(result).toEqual(fakeClaim);
|
||||||
});
|
});
|
||||||
@@ -1179,12 +1247,18 @@ describe('apple signin auth adapter', () => {
|
|||||||
iss: 'https://not.apple.com',
|
iss: 'https://not.apple.com',
|
||||||
sub: 'the_user_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);
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apple.validateAuthData(
|
await apple.validateAuthData(
|
||||||
{ id: 'the_user_id', token: 'the_token' },
|
{ id: 'the_user_id', token: 'the_token' },
|
||||||
{ client_id: 'secret' }
|
{ clientId: 'secret' }
|
||||||
);
|
);
|
||||||
fail();
|
fail();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1200,12 +1274,18 @@ describe('apple signin auth adapter', () => {
|
|||||||
aud: 'invalid_client_id',
|
aud: 'invalid_client_id',
|
||||||
sub: 'the_user_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);
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apple.validateAuthData(
|
await apple.validateAuthData(
|
||||||
{ id: 'the_user_id', token: 'the_token' },
|
{ id: 'the_user_id', token: 'the_token' },
|
||||||
{ client_id: 'secret' }
|
{ clientId: 'secret' }
|
||||||
);
|
);
|
||||||
fail();
|
fail();
|
||||||
} catch (e) {
|
} 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', () => {
|
describe('Apple Game Center Auth adapter', () => {
|
||||||
const gcenter = require('../lib/Adapters/Auth/gcenter');
|
const gcenter = require('../lib/Adapters/Auth/gcenter');
|
||||||
|
|
||||||
|
|||||||
@@ -2,45 +2,68 @@
|
|||||||
// https://developer.apple.com/documentation/signinwithapplerestapi
|
// https://developer.apple.com/documentation/signinwithapplerestapi
|
||||||
|
|
||||||
const Parse = require('parse/node').Parse;
|
const Parse = require('parse/node').Parse;
|
||||||
const httpsRequest = require('./httpsRequest');
|
const jwksClient = require('jwks-rsa');
|
||||||
const NodeRSA = require('node-rsa');
|
const util = require('util');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
const TOKEN_ISSUER = 'https://appleid.apple.com';
|
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 () => {
|
const asyncGetSigningKeyFunction = util.promisify(client.getSigningKey);
|
||||||
let data;
|
|
||||||
|
let key;
|
||||||
try {
|
try {
|
||||||
data = await httpsRequest.get('https://appleid.apple.com/auth/keys');
|
key = await asyncGetSigningKeyFunction(keyId);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
if (currentKey) {
|
throw new Parse.Error(
|
||||||
return currentKey;
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
}
|
`Unable to find matching key for Key ID: ${keyId}`
|
||||||
throw e;
|
);
|
||||||
}
|
}
|
||||||
|
return key;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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) {
|
if (!token) {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
'id token is invalid for this user.'
|
'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) {
|
if (jwtClaims.iss !== TOKEN_ISSUER) {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
@@ -54,10 +77,10 @@ const verifyIdToken = async ({ token, id }, clientID) => {
|
|||||||
`auth data is invalid for this user.`
|
`auth data is invalid for this user.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (clientID !== undefined && jwtClaims.aud !== clientID) {
|
if (clientId !== undefined && jwtClaims.aud !== clientId) {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
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;
|
return jwtClaims;
|
||||||
@@ -65,7 +88,7 @@ const verifyIdToken = async ({ token, id }, clientID) => {
|
|||||||
|
|
||||||
// Returns a promise that fulfills if this id token is valid
|
// Returns a promise that fulfills if this id token is valid
|
||||||
function validateAuthData(authData, options = {}) {
|
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.
|
// Returns a promise that fulfills if this app id is valid.
|
||||||
|
|||||||
Reference in New Issue
Block a user