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:
53
package-lock.json
generated
53
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
58
src/Adapters/Auth/apple-signin.js
Normal file
58
src/Adapters/Auth/apple-signin.js
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user