Sign in with Apple Auth Provider (#5694)

* Sign in with Apple Auth Provider

Closes: https://github.com/parse-community/parse-server/issues/5632

Should work out of the box.

* remove required options
This commit is contained in:
Diamond Lewis
2019-06-19 16:05:09 -05:00
committed by GitHub
parent 947c6beede
commit fcdf2d7947
4 changed files with 162 additions and 34 deletions

53
package-lock.json generated
View File

@@ -4292,8 +4292,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@@ -4314,14 +4313,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -4336,20 +4333,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -4466,8 +4460,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@@ -4479,7 +4472,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -4494,7 +4486,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -4502,14 +4493,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -4528,7 +4517,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -4609,8 +4597,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -4622,7 +4609,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -4708,8 +4694,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -4745,7 +4730,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -4765,7 +4749,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -4809,14 +4792,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},
@@ -6977,7 +6958,6 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -6986,8 +6966,7 @@
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"optional": true
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
}
}
},
@@ -7511,6 +7490,14 @@
}
}
},
"node-rsa": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.0.5.tgz",
"integrity": "sha512-9o51yfV167CtQANnuAf+5owNs7aIMsAKVLhNaKuRxihsUUnfoBMN5OTVOK/2mHSOWaWq9zZBiRM3bHORbTZqrg==",
"requires": {
"asn1": "^0.2.4"
}
},
"nopt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",

View File

@@ -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",

View File

@@ -17,6 +17,7 @@ const responses = {
describe('AuthenticationProviders', function() {
[
'apple-signin',
'facebook',
'facebookaccountkit',
'github',
@@ -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(
@@ -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'
);
}
});
});

View File

@@ -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,
};