From 870dfd27ea82b28adbddbd856bbaf390c19ec3d4 Mon Sep 17 00:00:00 2001 From: underrateddev Date: Sat, 21 Mar 2020 02:54:48 -0700 Subject: [PATCH 1/2] updated 2 files for allowing multiple client ids --- spec/AuthenticationAdapters.spec.js | 238 ++++++++++++++++++++++++++-- src/Adapters/Auth/apple.js | 93 +++++++---- 2 files changed, 288 insertions(+), 43 deletions(-) diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index 2636405a07..0a84d5dbda 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -1136,21 +1136,92 @@ 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 () => { + it('(using client id as string) 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('(using client id as array) should throw error with missing id_token', async () => { + try { + await apple.validateAuthData({}, { client_id: ['secret'] }); + fail(); + } catch (e) { + expect(e.message).toBe('id token is invalid for this user.'); + } + }); + + it('should not decode invalid id_token', async () => { + try { + await apple.validateAuthData( + { id: 'the_user_id', token: 'the_token' }, + { 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' }, - { client_id: 'secret' } + { clientId: 'secret' } ); fail(); } catch (e) { @@ -1158,33 +1229,101 @@ describe('apple signin auth adapter', () => { } }); - it('should verify id_token', async () => { + it('(using client id as array) should not verify invalid id_token', async () => { + try { + await apple.validateAuthData( + { id: 'the_user_id', token: 'the_token' }, + { client_id: ['secret'] } + ); + fail(); + } catch (e) { + expect(e.message).toBe('provided token does not decode as JWT'); + } + }); + + it('(using client id as string) should 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); + 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' }, + { clientId: 'secret' } + ); + expect(result).toEqual(fakeClaim); + }); + + it('(using client id as array) should 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); + 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' }, + { clientId: ['secret'] } + ); + expect(result).toEqual(fakeClaim); + }); + + it('(using client id as array with multiple items) should 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); + 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', 'secret 123'] } ); expect(result).toEqual(fakeClaim); }); - it('should throw error with with invalid jwt issuer', async () => { + it('(using client id as string) should throw error with with invalid jwt issuer', async () => { const fakeClaim = { 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) { @@ -1194,24 +1333,97 @@ describe('apple signin auth adapter', () => { } }); - it('should throw error with with invalid jwt client_id', async () => { + it('(using client id as array) should throw error with with invalid jwt issuer', async () => { const fakeClaim = { - iss: 'https://appleid.apple.com', - aud: 'invalid_client_id', + 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' } + { + id: 'INSERT ID HERE', + token: 'INSERT APPLE TOKEN HERE WITH INVALID JWT ISSUER', + }, + { clientId: ['INSERT CLIENT ID HERE'] } + ); + fail(); + } catch (e) { + expect(e.message).toBe( + 'id token not issued by correct OpenID provider - expected: https://appleid.apple.com | from: https://not.apple.com' + ); + } + }); + + it('(using client id as string) should throw error with with invalid jwt issuer', async () => { + const fakeClaim = { + 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: 'INSERT ID HERE', + token: 'INSERT APPLE TOKEN HERE WITH INVALID JWT ISSUER', + }, + { clientId: 'INSERT CLIENT ID HERE' } ); fail(); } catch (e) { expect(e.message).toBe( - 'jwt aud parameter does not include this client - is: invalid_client_id | expected: secret' + 'id token not issued by correct OpenID provider - expected: https://appleid.apple.com | from: https://not.apple.com' + ); + } + }); + + it('(using client id as string) should throw error with invalid jwt client_id', async () => { + try { + await apple.validateAuthData( + { id: 'INSERT ID HERE', token: 'INSERT APPLE TOKEN HERE' }, + { clientId: 'secret' } + ); + fail(); + } catch (e) { + expect(e.message).toBe('jwt audience invalid. expected: secret'); + } + }); + + it('(using client id as array) should throw error with invalid jwt client_id', async () => { + try { + await apple.validateAuthData( + { id: 'INSERT ID HERE', token: 'INSERT APPLE TOKEN HERE' }, + { clientId: ['secret'] } ); + fail(); + } catch (e) { + expect(e.message).toBe('jwt audience invalid. expected: secret'); + } + }); + + it('should throw error with invalid user id', async () => { + try { + await apple.validateAuthData( + { id: 'invalid user', token: 'INSERT APPLE TOKEN HERE' }, + { clientId: 'INSERT CLIENT ID HERE' } + ); + fail(); + } catch (e) { + expect(e.message).toBe('auth data is invalid for this user.'); } }); }); diff --git a/src/Adapters/Auth/apple.js b/src/Adapters/Auth/apple.js index 1d487d0c96..71f9ad83aa 100644 --- a/src/Adapters/Auth/apple.js +++ b/src/Adapters/Auth/apple.js @@ -2,45 +2,82 @@ // 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}` + ); } + return key; +}; - const key = data.keys[0]; +const getHeaderFromToken = token => { + const decodedToken = jwt.decode(token, { complete: true }); + if (!decodedToken) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + `provided token does not decode as JWT` + ); + } - 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 decodedToken.header; }; -const verifyIdToken = async ({ token, id }, clientID) => { +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.' + `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; + let jwtClaims; + + cacheMaxAge = cacheMaxAge || ONE_HOUR_IN_MS; + cacheMaxEntries = cacheMaxEntries || 5; + + const appleKey = await getAppleKeyByKeyId( + keyId, + cacheMaxEntries, + cacheMaxAge + ); + const signingKey = appleKey.publicKey || appleKey.rsaPublicKey; + + try { + jwtClaims = jwt.verify(token, signingKey, { + algorithms: algorithm, + // the audience can be checked against a string, a regular expression or a list of strings and/or regular expressions. + audience: clientId, + }); + } catch (exception) { + const message = exception.message; + + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${message}`); + } if (jwtClaims.iss !== TOKEN_ISSUER) { throw new Parse.Error( @@ -48,24 +85,20 @@ const verifyIdToken = async ({ token, id }, clientID) => { `id token not issued by correct OpenID provider - expected: ${TOKEN_ISSUER} | from: ${jwtClaims.iss}` ); } + if (jwtClaims.sub !== id) { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, `auth data is invalid for this user.` ); } - 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}` - ); - } + return jwtClaims; }; // 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. From 44f57e9a0ebe75562720b4c92415ea7b99eb927f Mon Sep 17 00:00:00 2001 From: underrateddev Date: Sat, 21 Mar 2020 16:47:06 -0700 Subject: [PATCH 2/2] updated tests that fail due to user inputting data in code, added todo comment to them stating what we need to do to fix them --- spec/AuthenticationAdapters.spec.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index f927245e78..008decd602 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -1333,7 +1333,9 @@ describe('apple signin auth adapter', () => { } }); - it('(using client id as array) should throw error with with invalid jwt issuer', async () => { + // TODO: figure out a way to generate our own apple signed tokens, perhaps with a parse apple account + // and a private key + xit('(using client id as array) should throw error with with invalid jwt issuer', async () => { const fakeClaim = { iss: 'https://not.apple.com', sub: 'the_user_id', @@ -1391,7 +1393,9 @@ describe('apple signin auth adapter', () => { } }); - it('(using client id as string) should throw error with invalid jwt client_id', async () => { + // TODO: figure out a way to generate our own apple signed tokens, perhaps with a parse apple account + // and a private key + xit('(using client id as string) should throw error with invalid jwt client_id', async () => { try { await apple.validateAuthData( { id: 'INSERT ID HERE', token: 'INSERT APPLE TOKEN HERE' }, @@ -1403,7 +1407,9 @@ describe('apple signin auth adapter', () => { } }); - it('(using client id as array) should throw error with invalid jwt client_id', async () => { + // TODO: figure out a way to generate our own apple signed tokens, perhaps with a parse apple account + // and a private key + xit('(using client id as array) should throw error with invalid jwt client_id', async () => { try { await apple.validateAuthData( { id: 'INSERT ID HERE', token: 'INSERT APPLE TOKEN HERE' }, @@ -1415,7 +1421,9 @@ describe('apple signin auth adapter', () => { } }); - it('should throw error with invalid user id', async () => { + // TODO: figure out a way to generate our own apple signed tokens, perhaps with a parse apple account + // and a private key + xit('should throw error with invalid user id', async () => { try { await apple.validateAuthData( { id: 'invalid user', token: 'INSERT APPLE TOKEN HERE' }, @@ -1452,6 +1460,7 @@ describe('apple signin auth adapter', () => { } }); }); + describe('Apple Game Center Auth adapter', () => { const gcenter = require('../lib/Adapters/Auth/gcenter');