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() {
|
describe('AuthenticationProviders', function() {
|
||||||
[
|
[
|
||||||
'apple',
|
'apple',
|
||||||
|
'gcenter',
|
||||||
'facebook',
|
'facebook',
|
||||||
'facebookaccountkit',
|
'facebookaccountkit',
|
||||||
'github',
|
'github',
|
||||||
@@ -39,7 +40,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
'weibo',
|
'weibo',
|
||||||
'phantauth',
|
'phantauth',
|
||||||
'microsoft',
|
'microsoft',
|
||||||
].map(function (providerName) {
|
].map(function(providerName) {
|
||||||
it('Should validate structure of ' + providerName, done => {
|
it('Should validate structure of ' + providerName, done => {
|
||||||
const provider = require('../lib/Adapters/Auth/' + providerName);
|
const provider = require('../lib/Adapters/Auth/' + providerName);
|
||||||
jequal(typeof provider.validateAuthData, 'function');
|
jequal(typeof provider.validateAuthData, 'function');
|
||||||
@@ -57,7 +58,8 @@ describe('AuthenticationProviders', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should provide the right responses for adapter ${providerName}`, async () => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(
|
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', () => {
|
describe('phant auth adapter', () => {
|
||||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||||
|
|
||||||
@@ -1205,8 +1268,24 @@ describe('microsoft graph auth adapter', () => {
|
|||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||||
return Promise.resolve({ id: 'userId', mail: 'userMail' });
|
return Promise.resolve({ id: 'userId', mail: 'userMail' });
|
||||||
});
|
});
|
||||||
await microsoft.validateAuthData(
|
await microsoft.validateAuthData({
|
||||||
{ id: 'userId', access_token: 'the_token' }
|
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 Parse = require('parse/node').Parse;
|
||||||
const httpsRequest = require('./httpsRequest');
|
const httpsRequest = require('./httpsRequest');
|
||||||
const NodeRSA = require('node-rsa');
|
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';
|
import loadAdapter from '../AdapterLoader';
|
||||||
|
|
||||||
const apple = require('./apple');
|
const apple = require('./apple');
|
||||||
|
const gcenter = require('./gcenter');
|
||||||
const facebook = require('./facebook');
|
const facebook = require('./facebook');
|
||||||
const facebookaccountkit = require('./facebookaccountkit');
|
const facebookaccountkit = require('./facebookaccountkit');
|
||||||
const instagram = require('./instagram');
|
const instagram = require('./instagram');
|
||||||
@@ -33,6 +34,7 @@ const anonymous = {
|
|||||||
|
|
||||||
const providers = {
|
const providers = {
|
||||||
apple,
|
apple,
|
||||||
|
gcenter,
|
||||||
facebook,
|
facebook,
|
||||||
facebookaccountkit,
|
facebookaccountkit,
|
||||||
instagram,
|
instagram,
|
||||||
|
|||||||
Reference in New Issue
Block a user