Support Apple Game Center Auth (#6143)
Fixes: https://github.com/parse-community/parse-server/issues/5984
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
125
src/Adapters/Auth/gcenter.js
Normal file
125
src/Adapters/Auth/gcenter.js
Normal 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,
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user