Skip to content

Sign in with Apple Auth Provider #5694

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

Merged
merged 2 commits into from
Jun 19, 2019
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
53 changes: 20 additions & 33 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
"express": "4.17.1",
"follow-redirects": "1.7.0",
"intersect": "1.0.1",
"jsonwebtoken": "8.5.1",
"lodash": "4.17.11",
"lru-cache": "5.1.1",
"mime": "2.4.4",
"mongodb": "3.2.7",
"node-rsa": "1.0.5",
"parse": "2.4.0",
"pg-promise": "8.7.2",
"redis": "2.8.0",
Expand Down
83 changes: 82 additions & 1 deletion spec/AuthenticationAdapters.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const responses = {

describe('AuthenticationProviders', function() {
[
'apple-signin',
'facebook',
'facebookaccountkit',
'github',
Expand Down Expand Up @@ -50,7 +51,7 @@ describe('AuthenticationProviders', function() {
});

it(`should provide the right responses for adapter ${providerName}`, async () => {
if (providerName === 'twitter') {
if (providerName === 'twitter' || providerName === 'apple-signin') {
return;
}
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(
Expand Down Expand Up @@ -1033,3 +1034,83 @@ describe('oauth2 auth adapter', () => {
}
});
});

describe('apple signin auth adapter', () => {
const apple = require('../lib/Adapters/Auth/apple-signin');
const jwt = require('jsonwebtoken');

it('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 verify invalid id_token', async () => {
try {
await apple.validateAuthData(
{ id_token: 'the_token' },
{ client_id: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('jwt malformed');
}
});

it('should verify id_token', async () => {
const fakeClaim = {
iss: 'https://appleid.apple.com',
aud: 'secret',
exp: Date.now(),
};
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);

const result = await apple.validateAuthData(
{ id_token: 'the_token' },
{ client_id: 'secret' }
);
expect(result).toEqual(fakeClaim);
});

it('should throw error with with invalid jwt issuer', async () => {
const fakeClaim = {
iss: 'https://not.apple.com',
};
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);

try {
await apple.validateAuthData(
{ id_token: 'the_token' },
{ client_id: 'secret' }
);
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('should throw error with with invalid jwt client_id', async () => {
const fakeClaim = {
iss: 'https://appleid.apple.com',
aud: 'invalid_client_id',
};
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);

try {
await apple.validateAuthData(
{ id_token: 'the_token' },
{ client_id: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe(
'jwt aud parameter does not include this client - is: invalid_client_id | expected: secret'
);
}
});
});
58 changes: 58 additions & 0 deletions src/Adapters/Auth/apple-signin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const Parse = require('parse/node').Parse;
const httpsRequest = require('./httpsRequest');
const NodeRSA = require('node-rsa');
const jwt = require('jsonwebtoken');

const TOKEN_ISSUER = 'https://appleid.apple.com';

const getApplePublicKey = async () => {
const data = await httpsRequest.get('https://appleid.apple.com/auth/keys');
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'
);
return pubKey.exportKey(['public']);
};

const verifyIdToken = async (token, clientID) => {
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' });

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 (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.id_token, options.client_id);
}

// Returns a promise that fulfills if this app id is valid.
function validateAppId() {
return Promise.resolve();
}

module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData,
};