Skip to content

Commit

Permalink
Sign in with Apple Auth Provider (#5694)
Browse files Browse the repository at this point in the history
* Sign in with Apple Auth Provider

Closes: #5632

Should work out of the box.

* remove required options
  • Loading branch information
dplewis authored Jun 19, 2019
1 parent 947c6be commit fcdf2d7
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 34 deletions.
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,
};

0 comments on commit fcdf2d7

Please sign in to comment.