Skip to content

Commit fcdf2d7

Browse files
authored
Sign in with Apple Auth Provider (#5694)
* Sign in with Apple Auth Provider Closes: #5632 Should work out of the box. * remove required options
1 parent 947c6be commit fcdf2d7

File tree

4 files changed

+162
-34
lines changed

4 files changed

+162
-34
lines changed

package-lock.json

Lines changed: 20 additions & 33 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@
3030
"express": "4.17.1",
3131
"follow-redirects": "1.7.0",
3232
"intersect": "1.0.1",
33+
"jsonwebtoken": "8.5.1",
3334
"lodash": "4.17.11",
3435
"lru-cache": "5.1.1",
3536
"mime": "2.4.4",
3637
"mongodb": "3.2.7",
38+
"node-rsa": "1.0.5",
3739
"parse": "2.4.0",
3840
"pg-promise": "8.7.2",
3941
"redis": "2.8.0",

spec/AuthenticationAdapters.spec.js

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const responses = {
1717

1818
describe('AuthenticationProviders', function() {
1919
[
20+
'apple-signin',
2021
'facebook',
2122
'facebookaccountkit',
2223
'github',
@@ -50,7 +51,7 @@ describe('AuthenticationProviders', function() {
5051
});
5152

5253
it(`should provide the right responses for adapter ${providerName}`, async () => {
53-
if (providerName === 'twitter') {
54+
if (providerName === 'twitter' || providerName === 'apple-signin') {
5455
return;
5556
}
5657
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(
@@ -1033,3 +1034,83 @@ describe('oauth2 auth adapter', () => {
10331034
}
10341035
});
10351036
});
1037+
1038+
describe('apple signin auth adapter', () => {
1039+
const apple = require('../lib/Adapters/Auth/apple-signin');
1040+
const jwt = require('jsonwebtoken');
1041+
1042+
it('should throw error with missing id_token', async () => {
1043+
try {
1044+
await apple.validateAuthData({}, { client_id: 'secret' });
1045+
fail();
1046+
} catch (e) {
1047+
expect(e.message).toBe('id_token is invalid for this user.');
1048+
}
1049+
});
1050+
1051+
it('should not verify invalid id_token', async () => {
1052+
try {
1053+
await apple.validateAuthData(
1054+
{ id_token: 'the_token' },
1055+
{ client_id: 'secret' }
1056+
);
1057+
fail();
1058+
} catch (e) {
1059+
expect(e.message).toBe('jwt malformed');
1060+
}
1061+
});
1062+
1063+
it('should verify id_token', async () => {
1064+
const fakeClaim = {
1065+
iss: 'https://appleid.apple.com',
1066+
aud: 'secret',
1067+
exp: Date.now(),
1068+
};
1069+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1070+
1071+
const result = await apple.validateAuthData(
1072+
{ id_token: 'the_token' },
1073+
{ client_id: 'secret' }
1074+
);
1075+
expect(result).toEqual(fakeClaim);
1076+
});
1077+
1078+
it('should throw error with with invalid jwt issuer', async () => {
1079+
const fakeClaim = {
1080+
iss: 'https://not.apple.com',
1081+
};
1082+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1083+
1084+
try {
1085+
await apple.validateAuthData(
1086+
{ id_token: 'the_token' },
1087+
{ client_id: 'secret' }
1088+
);
1089+
fail();
1090+
} catch (e) {
1091+
expect(e.message).toBe(
1092+
'id_token not issued by correct OpenID provider - expected: https://appleid.apple.com | from: https://not.apple.com'
1093+
);
1094+
}
1095+
});
1096+
1097+
it('should throw error with with invalid jwt client_id', async () => {
1098+
const fakeClaim = {
1099+
iss: 'https://appleid.apple.com',
1100+
aud: 'invalid_client_id',
1101+
};
1102+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1103+
1104+
try {
1105+
await apple.validateAuthData(
1106+
{ id_token: 'the_token' },
1107+
{ client_id: 'secret' }
1108+
);
1109+
fail();
1110+
} catch (e) {
1111+
expect(e.message).toBe(
1112+
'jwt aud parameter does not include this client - is: invalid_client_id | expected: secret'
1113+
);
1114+
}
1115+
});
1116+
});

src/Adapters/Auth/apple-signin.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const Parse = require('parse/node').Parse;
2+
const httpsRequest = require('./httpsRequest');
3+
const NodeRSA = require('node-rsa');
4+
const jwt = require('jsonwebtoken');
5+
6+
const TOKEN_ISSUER = 'https://appleid.apple.com';
7+
8+
const getApplePublicKey = async () => {
9+
const data = await httpsRequest.get('https://appleid.apple.com/auth/keys');
10+
const key = data.keys[0];
11+
12+
const pubKey = new NodeRSA();
13+
pubKey.importKey(
14+
{ n: Buffer.from(key.n, 'base64'), e: Buffer.from(key.e, 'base64') },
15+
'components-public'
16+
);
17+
return pubKey.exportKey(['public']);
18+
};
19+
20+
const verifyIdToken = async (token, clientID) => {
21+
if (!token) {
22+
throw new Parse.Error(
23+
Parse.Error.OBJECT_NOT_FOUND,
24+
'id_token is invalid for this user.'
25+
);
26+
}
27+
const applePublicKey = await getApplePublicKey();
28+
const jwtClaims = jwt.verify(token, applePublicKey, { algorithms: 'RS256' });
29+
30+
if (jwtClaims.iss !== TOKEN_ISSUER) {
31+
throw new Parse.Error(
32+
Parse.Error.OBJECT_NOT_FOUND,
33+
`id_token not issued by correct OpenID provider - expected: ${TOKEN_ISSUER} | from: ${jwtClaims.iss}`
34+
);
35+
}
36+
if (clientID !== undefined && jwtClaims.aud !== clientID) {
37+
throw new Parse.Error(
38+
Parse.Error.OBJECT_NOT_FOUND,
39+
`jwt aud parameter does not include this client - is: ${jwtClaims.aud} | expected: ${clientID}`
40+
);
41+
}
42+
return jwtClaims;
43+
};
44+
45+
// Returns a promise that fulfills if this id_token is valid
46+
function validateAuthData(authData, options = {}) {
47+
return verifyIdToken(authData.id_token, options.client_id);
48+
}
49+
50+
// Returns a promise that fulfills if this app id is valid.
51+
function validateAppId() {
52+
return Promise.resolve();
53+
}
54+
55+
module.exports = {
56+
validateAppId: validateAppId,
57+
validateAuthData: validateAuthData,
58+
};

0 commit comments

Comments
 (0)