diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index fc7ea53456..008decd602 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -1138,7 +1138,7 @@ describe('apple signin auth adapter', () => { 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({}, { clientId: 'secret' }); fail(); @@ -1147,6 +1147,15 @@ describe('apple signin auth adapter', () => { } }); + 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( @@ -1220,7 +1229,19 @@ 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', @@ -1242,7 +1263,51 @@ describe('apple signin auth adapter', () => { expect(result).toEqual(fakeClaim); }); - it('should throw error with with invalid jwt issuer', async () => { + 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' }, + { clientId: ['secret', 'secret 123'] } + ); + expect(result).toEqual(fakeClaim); + }); + + 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', @@ -1268,10 +1333,11 @@ describe('apple signin auth adapter', () => { } }); - it('should throw error with 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 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' } }; @@ -1284,17 +1350,91 @@ describe('apple signin auth adapter', () => { try { await apple.validateAuthData( - { id: 'the_user_id', token: 'the_token' }, - { clientId: '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( - '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 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( + 'id token not issued by correct OpenID provider - expected: https://appleid.apple.com | from: https://not.apple.com' + ); + } + }); + + // 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' }, + { clientId: 'secret' } + ); + fail(); + } catch (e) { + expect(e.message).toBe('jwt audience invalid. expected: secret'); + } + }); + + // 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' }, + { clientId: ['secret'] } + ); + fail(); + } catch (e) { + expect(e.message).toBe('jwt audience invalid. expected: secret'); + } + }); + + // 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' }, + { clientId: 'INSERT CLIENT ID HERE' } + ); + fail(); + } catch (e) { + expect(e.message).toBe('auth data is invalid for this user.'); + } + }); + it('should throw error with with invalid user id', async () => { const fakeClaim = { iss: 'https://appleid.apple.com', @@ -1320,6 +1460,7 @@ describe('apple signin auth adapter', () => { } }); }); + describe('Apple Game Center Auth adapter', () => { const gcenter = require('../lib/Adapters/Auth/gcenter'); diff --git a/src/Adapters/Auth/apple.js b/src/Adapters/Auth/apple.js index 46ed01bf46..2731183b7f 100644 --- a/src/Adapters/Auth/apple.js +++ b/src/Adapters/Auth/apple.js @@ -33,8 +33,12 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => { const getHeaderFromToken = token => { const decodedToken = jwt.decode(token, { complete: true }); if (!decodedToken) { - throw Error('provided token does not decode as JWT'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + `provided token does not decode as JWT` + ); } + return decodedToken.header; }; @@ -45,12 +49,14 @@ const verifyIdToken = async ( 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 { kid: keyId, alg: algorithm } = getHeaderFromToken(token); const ONE_HOUR_IN_MS = 3600000; + let jwtClaims; + cacheMaxAge = cacheMaxAge || ONE_HOUR_IN_MS; cacheMaxEntries = cacheMaxEntries || 5; @@ -61,9 +67,17 @@ const verifyIdToken = async ( ); const signingKey = appleKey.publicKey || appleKey.rsaPublicKey; - const jwtClaims = jwt.verify(token, signingKey, { - algorithms: algorithm, - }); + 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( @@ -71,18 +85,13 @@ const verifyIdToken = async ( `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; };