Skip to content

Commit c1a217c

Browse files
authored
Support Apple Game Center Auth (#6143)
Fixes: #5984
1 parent d7bcc72 commit c1a217c

File tree

5 files changed

+214
-23
lines changed

5 files changed

+214
-23
lines changed

spec/AuthenticationAdapters.spec.js

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const responses = {
2121
describe('AuthenticationProviders', function() {
2222
[
2323
'apple',
24+
'gcenter',
2425
'facebook',
2526
'facebookaccountkit',
2627
'github',
@@ -39,7 +40,7 @@ describe('AuthenticationProviders', function() {
3940
'weibo',
4041
'phantauth',
4142
'microsoft',
42-
].map(function (providerName) {
43+
].map(function(providerName) {
4344
it('Should validate structure of ' + providerName, done => {
4445
const provider = require('../lib/Adapters/Auth/' + providerName);
4546
jequal(typeof provider.validateAuthData, 'function');
@@ -57,7 +58,8 @@ describe('AuthenticationProviders', function() {
5758
});
5859

5960
it(`should provide the right responses for adapter ${providerName}`, async () => {
60-
if (providerName === 'twitter' || providerName === 'apple') {
61+
const noResponse = ['twitter', 'apple', 'gcenter'];
62+
if (noResponse.includes(providerName)) {
6163
return;
6264
}
6365
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(
@@ -1175,6 +1177,67 @@ describe('apple signin auth adapter', () => {
11751177
});
11761178
});
11771179

1180+
describe('Apple Game Center Auth adapter', () => {
1181+
const gcenter = require('../lib/Adapters/Auth/gcenter');
1182+
1183+
it('validateAuthData should validate', async () => {
1184+
// real token is used
1185+
const authData = {
1186+
id: 'G:1965586982',
1187+
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
1188+
timestamp: 1565257031287,
1189+
signature:
1190+
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
1191+
salt: 'DzqqrQ==',
1192+
bundleId: 'cloud.xtralife.gamecenterauth',
1193+
};
1194+
1195+
try {
1196+
await gcenter.validateAuthData(authData);
1197+
} catch (e) {
1198+
fail();
1199+
}
1200+
});
1201+
1202+
it('validateAuthData invalid signature id', async () => {
1203+
const authData = {
1204+
id: 'G:1965586982',
1205+
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
1206+
timestamp: 1565257031287,
1207+
signature: '1234',
1208+
salt: 'DzqqrQ==',
1209+
bundleId: 'cloud.xtralife.gamecenterauth',
1210+
};
1211+
1212+
try {
1213+
await gcenter.validateAuthData(authData);
1214+
fail();
1215+
} catch (e) {
1216+
expect(e.message).toBe('Apple Game Center - invalid signature');
1217+
}
1218+
});
1219+
1220+
it('validateAuthData invalid public key url', async () => {
1221+
const authData = {
1222+
id: 'G:1965586982',
1223+
publicKeyUrl: 'invalid.com',
1224+
timestamp: 1565257031287,
1225+
signature: '1234',
1226+
salt: 'DzqqrQ==',
1227+
bundleId: 'cloud.xtralife.gamecenterauth',
1228+
};
1229+
1230+
try {
1231+
await gcenter.validateAuthData(authData);
1232+
fail();
1233+
} catch (e) {
1234+
expect(e.message).toBe(
1235+
'Apple Game Center - invalid publicKeyUrl: invalid.com'
1236+
);
1237+
}
1238+
});
1239+
});
1240+
11781241
describe('phant auth adapter', () => {
11791242
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
11801243

@@ -1205,8 +1268,24 @@ describe('microsoft graph auth adapter', () => {
12051268
spyOn(httpsRequest, 'get').and.callFake(() => {
12061269
return Promise.resolve({ id: 'userId', mail: 'userMail' });
12071270
});
1208-
await microsoft.validateAuthData(
1209-
{ id: 'userId', access_token: 'the_token' }
1210-
);
1271+
await microsoft.validateAuthData({
1272+
id: 'userId',
1273+
access_token: 'the_token',
1274+
});
1275+
});
1276+
1277+
it('should fail to validate Microsoft Graph auth with bad token', done => {
1278+
const authData = {
1279+
id: 'fake-id',
1280+
1281+
access_token: 'very.long.bad.token',
1282+
};
1283+
microsoft.validateAuthData(authData).then(done.fail, err => {
1284+
expect(err.code).toBe(101);
1285+
expect(err.message).toBe(
1286+
'Microsoft Graph auth is invalid for this user.'
1287+
);
1288+
done();
1289+
});
12111290
});
12121291
});

spec/MicrosoftAuth.spec.js

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/Adapters/Auth/apple.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Apple SignIn Auth
2+
// https://developer.apple.com/documentation/signinwithapplerestapi
3+
14
const Parse = require('parse/node').Parse;
25
const httpsRequest = require('./httpsRequest');
36
const NodeRSA = require('node-rsa');

src/Adapters/Auth/gcenter.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/* Apple Game Center Auth
2+
https://developer.apple.com/documentation/gamekit/gklocalplayer/1515407-generateidentityverificationsign#discussion
3+
4+
const authData = {
5+
publicKeyUrl: 'https://valid.apple.com/public/timeout.cer',
6+
timestamp: 1460981421303,
7+
signature: 'PoDwf39DCN464B49jJCU0d9Y0J',
8+
salt: 'saltST==',
9+
bundleId: 'com.valid.app'
10+
id: 'playerId',
11+
};
12+
*/
13+
14+
const { Parse } = require('parse/node');
15+
const crypto = require('crypto');
16+
const https = require('https');
17+
const url = require('url');
18+
19+
const cache = {}; // (publicKey -> cert) cache
20+
21+
function verifyPublicKeyUrl(publicKeyUrl) {
22+
const parsedUrl = url.parse(publicKeyUrl);
23+
if (parsedUrl.protocol !== 'https:') {
24+
return false;
25+
}
26+
const hostnameParts = parsedUrl.hostname.split('.');
27+
const length = hostnameParts.length;
28+
const domainParts = hostnameParts.slice(length - 2, length);
29+
const domain = domainParts.join('.');
30+
return domain === 'apple.com';
31+
}
32+
33+
function convertX509CertToPEM(X509Cert) {
34+
const pemPreFix = '-----BEGIN CERTIFICATE-----\n';
35+
const pemPostFix = '-----END CERTIFICATE-----';
36+
37+
const base64 = X509Cert;
38+
const certBody = base64.match(new RegExp('.{0,64}', 'g')).join('\n');
39+
40+
return pemPreFix + certBody + pemPostFix;
41+
}
42+
43+
function getAppleCertificate(publicKeyUrl) {
44+
if (!verifyPublicKeyUrl(publicKeyUrl)) {
45+
throw new Parse.Error(
46+
Parse.Error.OBJECT_NOT_FOUND,
47+
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
48+
);
49+
}
50+
if (cache[publicKeyUrl]) {
51+
return cache[publicKeyUrl];
52+
}
53+
return new Promise((resolve, reject) => {
54+
https
55+
.get(publicKeyUrl, res => {
56+
let data = '';
57+
res.on('data', chunk => {
58+
data += chunk.toString('base64');
59+
});
60+
res.on('end', () => {
61+
const cert = convertX509CertToPEM(data);
62+
if (res.headers['cache-control']) {
63+
var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
64+
if (expire) {
65+
cache[publicKeyUrl] = cert;
66+
// we'll expire the cache entry later, as per max-age
67+
setTimeout(() => {
68+
delete cache[publicKeyUrl];
69+
}, parseInt(expire[1], 10) * 1000);
70+
}
71+
}
72+
resolve(cert);
73+
});
74+
})
75+
.on('error', reject);
76+
});
77+
}
78+
79+
function convertTimestampToBigEndian(timestamp) {
80+
const buffer = new Buffer(8);
81+
buffer.fill(0);
82+
83+
const high = ~~(timestamp / 0xffffffff);
84+
const low = timestamp % (0xffffffff + 0x1);
85+
86+
buffer.writeUInt32BE(parseInt(high, 10), 0);
87+
buffer.writeUInt32BE(parseInt(low, 10), 4);
88+
89+
return buffer;
90+
}
91+
92+
function verifySignature(publicKey, authData) {
93+
const verifier = crypto.createVerify('sha256');
94+
verifier.update(authData.playerId, 'utf8');
95+
verifier.update(authData.bundleId, 'utf8');
96+
verifier.update(convertTimestampToBigEndian(authData.timestamp));
97+
verifier.update(authData.salt, 'base64');
98+
99+
if (!verifier.verify(publicKey, authData.signature, 'base64')) {
100+
throw new Parse.Error(
101+
Parse.Error.OBJECT_NOT_FOUND,
102+
'Apple Game Center - invalid signature'
103+
);
104+
}
105+
}
106+
107+
// Returns a promise that fulfills if this user id is valid.
108+
async function validateAuthData(authData) {
109+
if (!authData.id) {
110+
return Promise.reject('Apple Game Center - authData id missing');
111+
}
112+
authData.playerId = authData.id;
113+
const publicKey = await getAppleCertificate(authData.publicKeyUrl);
114+
return verifySignature(publicKey, authData);
115+
}
116+
117+
// Returns a promise that fulfills if this app id is valid.
118+
function validateAppId() {
119+
return Promise.resolve();
120+
}
121+
122+
module.exports = {
123+
validateAppId,
124+
validateAuthData,
125+
};

src/Adapters/Auth/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import loadAdapter from '../AdapterLoader';
22

33
const apple = require('./apple');
4+
const gcenter = require('./gcenter');
45
const facebook = require('./facebook');
56
const facebookaccountkit = require('./facebookaccountkit');
67
const instagram = require('./instagram');
@@ -33,6 +34,7 @@ const anonymous = {
3334

3435
const providers = {
3536
apple,
37+
gcenter,
3638
facebook,
3739
facebookaccountkit,
3840
instagram,

0 commit comments

Comments
 (0)