Support Apple Game Center Auth (#6143)

Fixes: https://github.com/parse-community/parse-server/issues/5984
This commit is contained in:
Diamond Lewis
2019-10-18 19:04:01 -05:00
committed by GitHub
parent d7bcc72a8a
commit c1a217c6b8
5 changed files with 214 additions and 23 deletions

View File

@@ -21,6 +21,7 @@ const responses = {
describe('AuthenticationProviders', function() {
[
'apple',
'gcenter',
'facebook',
'facebookaccountkit',
'github',
@@ -39,7 +40,7 @@ describe('AuthenticationProviders', function() {
'weibo',
'phantauth',
'microsoft',
].map(function (providerName) {
].map(function(providerName) {
it('Should validate structure of ' + providerName, done => {
const provider = require('../lib/Adapters/Auth/' + providerName);
jequal(typeof provider.validateAuthData, 'function');
@@ -57,7 +58,8 @@ describe('AuthenticationProviders', function() {
});
it(`should provide the right responses for adapter ${providerName}`, async () => {
if (providerName === 'twitter' || providerName === 'apple') {
const noResponse = ['twitter', 'apple', 'gcenter'];
if (noResponse.includes(providerName)) {
return;
}
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(
@@ -1175,6 +1177,67 @@ describe('apple signin auth adapter', () => {
});
});
describe('Apple Game Center Auth adapter', () => {
const gcenter = require('../lib/Adapters/Auth/gcenter');
it('validateAuthData should validate', async () => {
// real token is used
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
timestamp: 1565257031287,
signature:
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};
try {
await gcenter.validateAuthData(authData);
} catch (e) {
fail();
}
});
it('validateAuthData invalid signature id', async () => {
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('Apple Game Center - invalid signature');
}
});
it('validateAuthData invalid public key url', async () => {
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'invalid.com',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe(
'Apple Game Center - invalid publicKeyUrl: invalid.com'
);
}
});
});
describe('phant auth adapter', () => {
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
@@ -1205,8 +1268,24 @@ describe('microsoft graph auth adapter', () => {
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve({ id: 'userId', mail: 'userMail' });
});
await microsoft.validateAuthData(
{ id: 'userId', access_token: 'the_token' }
);
await microsoft.validateAuthData({
id: 'userId',
access_token: 'the_token',
});
});
it('should fail to validate Microsoft Graph auth with bad token', done => {
const authData = {
id: 'fake-id',
mail: 'fake@mail.com',
access_token: 'very.long.bad.token',
};
microsoft.validateAuthData(authData).then(done.fail, err => {
expect(err.code).toBe(101);
expect(err.message).toBe(
'Microsoft Graph auth is invalid for this user.'
);
done();
});
});
});

View File

@@ -1,18 +0,0 @@
const microsoft = require('../lib/Adapters/Auth/microsoft');
describe('Microsoft Auth', () => {
it('should fail to validate Microsoft Graph auth with bad token', done => {
const authData = {
id: 'fake-id',
mail: 'fake@mail.com',
access_token: 'very.long.bad.token',
};
microsoft.validateAuthData(authData).then(done.fail, err => {
expect(err.code).toBe(101);
expect(err.message).toBe(
'Microsoft Graph auth is invalid for this user.'
);
done();
});
});
});

View File

@@ -1,3 +1,6 @@
// Apple SignIn Auth
// https://developer.apple.com/documentation/signinwithapplerestapi
const Parse = require('parse/node').Parse;
const httpsRequest = require('./httpsRequest');
const NodeRSA = require('node-rsa');

View File

@@ -0,0 +1,125 @@
/* Apple Game Center Auth
https://developer.apple.com/documentation/gamekit/gklocalplayer/1515407-generateidentityverificationsign#discussion
const authData = {
publicKeyUrl: 'https://valid.apple.com/public/timeout.cer',
timestamp: 1460981421303,
signature: 'PoDwf39DCN464B49jJCU0d9Y0J',
salt: 'saltST==',
bundleId: 'com.valid.app'
id: 'playerId',
};
*/
const { Parse } = require('parse/node');
const crypto = require('crypto');
const https = require('https');
const url = require('url');
const cache = {}; // (publicKey -> cert) cache
function verifyPublicKeyUrl(publicKeyUrl) {
const parsedUrl = url.parse(publicKeyUrl);
if (parsedUrl.protocol !== 'https:') {
return false;
}
const hostnameParts = parsedUrl.hostname.split('.');
const length = hostnameParts.length;
const domainParts = hostnameParts.slice(length - 2, length);
const domain = domainParts.join('.');
return domain === 'apple.com';
}
function convertX509CertToPEM(X509Cert) {
const pemPreFix = '-----BEGIN CERTIFICATE-----\n';
const pemPostFix = '-----END CERTIFICATE-----';
const base64 = X509Cert;
const certBody = base64.match(new RegExp('.{0,64}', 'g')).join('\n');
return pemPreFix + certBody + pemPostFix;
}
function getAppleCertificate(publicKeyUrl) {
if (!verifyPublicKeyUrl(publicKeyUrl)) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
);
}
if (cache[publicKeyUrl]) {
return cache[publicKeyUrl];
}
return new Promise((resolve, reject) => {
https
.get(publicKeyUrl, res => {
let data = '';
res.on('data', chunk => {
data += chunk.toString('base64');
});
res.on('end', () => {
const cert = convertX509CertToPEM(data);
if (res.headers['cache-control']) {
var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
if (expire) {
cache[publicKeyUrl] = cert;
// we'll expire the cache entry later, as per max-age
setTimeout(() => {
delete cache[publicKeyUrl];
}, parseInt(expire[1], 10) * 1000);
}
}
resolve(cert);
});
})
.on('error', reject);
});
}
function convertTimestampToBigEndian(timestamp) {
const buffer = new Buffer(8);
buffer.fill(0);
const high = ~~(timestamp / 0xffffffff);
const low = timestamp % (0xffffffff + 0x1);
buffer.writeUInt32BE(parseInt(high, 10), 0);
buffer.writeUInt32BE(parseInt(low, 10), 4);
return buffer;
}
function verifySignature(publicKey, authData) {
const verifier = crypto.createVerify('sha256');
verifier.update(authData.playerId, 'utf8');
verifier.update(authData.bundleId, 'utf8');
verifier.update(convertTimestampToBigEndian(authData.timestamp));
verifier.update(authData.salt, 'base64');
if (!verifier.verify(publicKey, authData.signature, 'base64')) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple Game Center - invalid signature'
);
}
}
// Returns a promise that fulfills if this user id is valid.
async function validateAuthData(authData) {
if (!authData.id) {
return Promise.reject('Apple Game Center - authData id missing');
}
authData.playerId = authData.id;
const publicKey = await getAppleCertificate(authData.publicKeyUrl);
return verifySignature(publicKey, authData);
}
// Returns a promise that fulfills if this app id is valid.
function validateAppId() {
return Promise.resolve();
}
module.exports = {
validateAppId,
validateAuthData,
};

View File

@@ -1,6 +1,7 @@
import loadAdapter from '../AdapterLoader';
const apple = require('./apple');
const gcenter = require('./gcenter');
const facebook = require('./facebook');
const facebookaccountkit = require('./facebookaccountkit');
const instagram = require('./instagram');
@@ -33,6 +34,7 @@ const anonymous = {
const providers = {
apple,
gcenter,
facebook,
facebookaccountkit,
instagram,