Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apple signin for allowing multiple client ids #6523

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 150 additions & 9 deletions spec/AuthenticationAdapters.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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(
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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' } };
Expand All @@ -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',
Expand All @@ -1320,6 +1460,7 @@ describe('apple signin auth adapter', () => {
}
});
});

describe('Apple Game Center Auth adapter', () => {
const gcenter = require('../lib/Adapters/Auth/gcenter');

Expand Down
31 changes: 20 additions & 11 deletions src/Adapters/Auth/apple.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand All @@ -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;

Expand All @@ -61,28 +67,31 @@ 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(
Parse.Error.OBJECT_NOT_FOUND,
`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;
};

Expand Down