fix: Authentication provider credentials are usable across Parse Server apps; fixes security vulnerability [GHSA-837q-jhwx-cmpv](https://github.com/parse-community/parse-server/security/advisories/GHSA-837q-jhwx-cmpv) (#9667)
This commit is contained in:
182
spec/Adapters/Auth/BaseCodeAdapter.spec.js
Normal file
182
spec/Adapters/Auth/BaseCodeAdapter.spec.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
const BaseAuthCodeAdapter = require('../../../lib/Adapters/Auth/BaseCodeAuthAdapter').default;
|
||||||
|
|
||||||
|
describe('BaseAuthCodeAdapter', function () {
|
||||||
|
let adapter;
|
||||||
|
const adapterName = 'TestAdapter';
|
||||||
|
const validOptions = {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
};
|
||||||
|
|
||||||
|
class TestAuthCodeAdapter extends BaseAuthCodeAdapter {
|
||||||
|
async getUserFromAccessToken(accessToken) {
|
||||||
|
if (accessToken === 'validAccessToken') {
|
||||||
|
return { id: 'validUserId' };
|
||||||
|
}
|
||||||
|
throw new Error('Invalid access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessTokenFromCode(authData) {
|
||||||
|
if (authData.code === 'validCode') {
|
||||||
|
return 'validAccessToken';
|
||||||
|
}
|
||||||
|
throw new Error('Invalid code');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new TestAuthCodeAdapter(adapterName);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateOptions', function () {
|
||||||
|
it('should throw error if options are missing', function () {
|
||||||
|
expect(() => adapter.validateOptions(null)).toThrowError(`${adapterName} options are required.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if clientId is missing in secure mode', function () {
|
||||||
|
expect(() =>
|
||||||
|
adapter.validateOptions({ clientSecret: 'validClientSecret' })
|
||||||
|
).toThrowError(`${adapterName} clientId is required.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if clientSecret is missing in secure mode', function () {
|
||||||
|
expect(() =>
|
||||||
|
adapter.validateOptions({ clientId: 'validClientId' })
|
||||||
|
).toThrowError(`${adapterName} clientSecret is required.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw error for valid options', function () {
|
||||||
|
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||||
|
expect(adapter.clientId).toBe('validClientId');
|
||||||
|
expect(adapter.clientSecret).toBe('validClientSecret');
|
||||||
|
expect(adapter.enableInsecureAuth).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow insecure mode without clientId or clientSecret', function () {
|
||||||
|
const options = { enableInsecureAuth: true };
|
||||||
|
expect(() => adapter.validateOptions(options)).not.toThrow();
|
||||||
|
expect(adapter.enableInsecureAuth).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('beforeFind', function () {
|
||||||
|
it('should throw error if code is missing in secure mode', async function () {
|
||||||
|
adapter.validateOptions(validOptions);
|
||||||
|
const authData = { access_token: 'validAccessToken' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||||
|
`${adapterName} code is required.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if access token is missing in insecure mode', async function () {
|
||||||
|
adapter.validateOptions({ enableInsecureAuth: true });
|
||||||
|
const authData = {};
|
||||||
|
|
||||||
|
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||||
|
`${adapterName} auth is invalid for this user.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if user ID does not match in insecure mode', async function () {
|
||||||
|
adapter.validateOptions({ enableInsecureAuth: true });
|
||||||
|
const authData = { id: 'invalidUserId', access_token: 'validAccessToken' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||||
|
`${adapterName} auth is invalid for this user.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process valid secure payload and update authData', async function () {
|
||||||
|
adapter.validateOptions(validOptions);
|
||||||
|
const authData = { code: 'validCode' };
|
||||||
|
|
||||||
|
await adapter.beforeFind(authData);
|
||||||
|
|
||||||
|
expect(authData.access_token).toBe('validAccessToken');
|
||||||
|
expect(authData.id).toBe('validUserId');
|
||||||
|
expect(authData.code).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process valid insecure payload', async function () {
|
||||||
|
adapter.validateOptions({ enableInsecureAuth: true });
|
||||||
|
const authData = { id: 'validUserId', access_token: 'validAccessToken' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserFromAccessToken', function () {
|
||||||
|
it('should throw error if not implemented in base class', async function () {
|
||||||
|
const baseAdapter = new BaseAuthCodeAdapter(adapterName);
|
||||||
|
|
||||||
|
await expectAsync(baseAdapter.getUserFromAccessToken('test')).toBeRejectedWithError(
|
||||||
|
'getUserFromAccessToken is not implemented'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return valid user for valid access token', async function () {
|
||||||
|
const user = await adapter.getUserFromAccessToken('validAccessToken', {});
|
||||||
|
expect(user).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid access token', async function () {
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken', {})).toBeRejectedWithError(
|
||||||
|
'Invalid access token'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAccessTokenFromCode', function () {
|
||||||
|
it('should throw error if not implemented in base class', async function () {
|
||||||
|
const baseAdapter = new BaseAuthCodeAdapter(adapterName);
|
||||||
|
|
||||||
|
await expectAsync(baseAdapter.getAccessTokenFromCode({ code: 'test' })).toBeRejectedWithError(
|
||||||
|
'getAccessTokenFromCode is not implemented'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return valid access token for valid code', async function () {
|
||||||
|
const accessToken = await adapter.getAccessTokenFromCode({ code: 'validCode' });
|
||||||
|
expect(accessToken).toBe('validAccessToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid code', async function () {
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode({ code: 'invalidCode' })).toBeRejectedWithError(
|
||||||
|
'Invalid code'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateLogin', function () {
|
||||||
|
it('should return user id from authData', function () {
|
||||||
|
const authData = { id: 'validUserId' };
|
||||||
|
const result = adapter.validateLogin(authData);
|
||||||
|
expect(result).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateSetUp', function () {
|
||||||
|
it('should return user id from authData', function () {
|
||||||
|
const authData = { id: 'validUserId' };
|
||||||
|
const result = adapter.validateSetUp(authData);
|
||||||
|
expect(result).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('afterFind', function () {
|
||||||
|
it('should return user id from authData', function () {
|
||||||
|
const authData = { id: 'validUserId' };
|
||||||
|
const result = adapter.afterFind(authData);
|
||||||
|
expect(result).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateUpdate', function () {
|
||||||
|
it('should return user id from authData', function () {
|
||||||
|
const authData = { id: 'validUserId' };
|
||||||
|
const result = adapter.validateUpdate(authData);
|
||||||
|
expect(result).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
220
spec/Adapters/Auth/gcenter.spec.js
Normal file
220
spec/Adapters/Auth/gcenter.spec.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const GameCenterAuth = require('../../../lib/Adapters/Auth/gcenter').default;
|
||||||
|
const { pki } = require('node-forge');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
describe('GameCenterAuth Adapter', function () {
|
||||||
|
let adapter;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new GameCenterAuth.constructor();
|
||||||
|
|
||||||
|
const gcProd4 = fs.readFileSync(path.resolve(__dirname, '../../support/cert/gc-prod-4.cer'));
|
||||||
|
const digicertPem = fs.readFileSync(path.resolve(__dirname, '../../support/cert/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem')).toString();
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
headers: new Map(),
|
||||||
|
arrayBuffer: () => Promise.resolve(
|
||||||
|
gcProd4.buffer.slice(gcProd4.byteOffset, gcProd4.byteOffset + gcProd4.length)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
headers: new Map([['content-type', 'application/x-pem-file'], ['content-length', digicertPem.length.toString()]]),
|
||||||
|
text: () => Promise.resolve(digicertPem),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test config failing due to missing params or wrong types', function () {
|
||||||
|
it('should throw error for invalid options', async function () {
|
||||||
|
const invalidOptions = [
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
{},
|
||||||
|
{ bundleId: '' },
|
||||||
|
{ enableInsecureAuth: false }, // Missing bundleId in secure mode
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const options of invalidOptions) {
|
||||||
|
expect(() => adapter.validateOptions(options)).withContext(JSON.stringify(options)).toThrow()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate options successfully with valid parameters', function () {
|
||||||
|
const validOptions = { bundleId: 'com.valid.app', enableInsecureAuth: false };
|
||||||
|
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test payload failing due to missing params or wrong types', function () {
|
||||||
|
it('should throw error for missing authData fields', async function () {
|
||||||
|
await expectAsync(adapter.validateAuthData({})).toBeRejectedWithError(
|
||||||
|
'AuthData id is missing.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test payload fails due to incorrect appId / certificate', function () {
|
||||||
|
it('should throw error for invalid publicKeyUrl', async function () {
|
||||||
|
const invalidPublicKeyUrl = 'https://malicious.url.com/key.cer';
|
||||||
|
|
||||||
|
spyOn(adapter, 'fetchCertificate').and.throwError(
|
||||||
|
new Error('Invalid publicKeyUrl')
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
adapter.getAppleCertificate(invalidPublicKeyUrl)
|
||||||
|
).toBeRejectedWithError('Invalid publicKeyUrl: https://malicious.url.com/key.cer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid signature verification', async function () {
|
||||||
|
const fakePublicKey = 'invalid-key';
|
||||||
|
const fakeAuthData = {
|
||||||
|
id: '1234567',
|
||||||
|
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||||
|
timestamp: 1460981421303,
|
||||||
|
salt: 'saltST==',
|
||||||
|
signature: 'invalidSignature',
|
||||||
|
};
|
||||||
|
|
||||||
|
spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve(fakePublicKey));
|
||||||
|
spyOn(adapter, 'verifySignature').and.throwError('Invalid signature.');
|
||||||
|
|
||||||
|
await expectAsync(adapter.validateAuthData(fakeAuthData)).toBeRejectedWithError(
|
||||||
|
'Invalid signature.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test payload passing', function () {
|
||||||
|
it('should successfully process valid payload and save auth data', async function () {
|
||||||
|
const validAuthData = {
|
||||||
|
id: '1234567',
|
||||||
|
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||||
|
timestamp: 1460981421303,
|
||||||
|
salt: 'saltST==',
|
||||||
|
signature: 'validSignature',
|
||||||
|
bundleId: 'com.valid.app',
|
||||||
|
};
|
||||||
|
|
||||||
|
spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve('validKey'));
|
||||||
|
spyOn(adapter, 'verifySignature').and.returnValue(true);
|
||||||
|
|
||||||
|
await expectAsync(adapter.validateAuthData(validAuthData)).toBeResolved();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Certificate and Signature Validation', function () {
|
||||||
|
it('should fetch and validate Apple certificate', async function () {
|
||||||
|
const certUrl = 'https://static.gc.apple.com/public-key/gc-prod-4.cer';
|
||||||
|
const mockCertificate = 'mockCertificate';
|
||||||
|
|
||||||
|
spyOn(adapter, 'fetchCertificate').and.returnValue(
|
||||||
|
Promise.resolve({ certificate: mockCertificate, headers: new Map() })
|
||||||
|
);
|
||||||
|
spyOn(pki, 'certificateFromPem').and.returnValue({});
|
||||||
|
|
||||||
|
adapter.cache[certUrl] = mockCertificate;
|
||||||
|
|
||||||
|
const cert = await adapter.getAppleCertificate(certUrl);
|
||||||
|
expect(cert).toBe(mockCertificate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should verify signature successfully', async function () {
|
||||||
|
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==',
|
||||||
|
};
|
||||||
|
|
||||||
|
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||||
|
adapter.enableInsecureAuth = false;
|
||||||
|
|
||||||
|
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||||
|
|
||||||
|
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||||
|
|
||||||
|
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not use bundle id from authData payload in secure mode', async function () {
|
||||||
|
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: 'com.example.insecure.app',
|
||||||
|
};
|
||||||
|
|
||||||
|
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||||
|
adapter.enableInsecureAuth = false;
|
||||||
|
|
||||||
|
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||||
|
|
||||||
|
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||||
|
|
||||||
|
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not use bundle id from authData payload in insecure mode', async function () {
|
||||||
|
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: 'com.example.insecure.app',
|
||||||
|
};
|
||||||
|
|
||||||
|
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||||
|
adapter.enableInsecureAuth = true;
|
||||||
|
|
||||||
|
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||||
|
|
||||||
|
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||||
|
|
||||||
|
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can use bundle id from authData payload in insecure mode', async function () {
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
adapter.enableInsecureAuth = true;
|
||||||
|
|
||||||
|
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||||
|
|
||||||
|
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||||
|
|
||||||
|
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
285
spec/Adapters/Auth/github.spec.js
Normal file
285
spec/Adapters/Auth/github.spec.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
const GitHubAdapter = require('../../../lib/Adapters/Auth/github').default;
|
||||||
|
|
||||||
|
describe('GitHubAdapter', function () {
|
||||||
|
let adapter;
|
||||||
|
const validOptions = {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new GitHubAdapter.constructor();
|
||||||
|
adapter.validateOptions(validOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAccessTokenFromCode', function () {
|
||||||
|
it('should fetch an access token successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://github.com/login/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const code = 'validCode';
|
||||||
|
const token = await adapter.getAccessTokenFromCode(code);
|
||||||
|
|
||||||
|
expect(token).toBe('mockAccessToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the response is not ok', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://github.com/login/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Bad Request',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const code = 'invalidCode';
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError(
|
||||||
|
'Failed to exchange code for token: Bad Request'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the response contains an error', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://github.com/login/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
error: 'invalid_grant',
|
||||||
|
error_description: 'Code is invalid',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const code = 'invalidCode';
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError('Code is invalid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserFromAccessToken', function () {
|
||||||
|
it('should fetch user data successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.github.com/user',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
id: 'mockUserId',
|
||||||
|
login: 'mockUserLogin',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'validAccessToken';
|
||||||
|
const user = await adapter.getUserFromAccessToken(accessToken);
|
||||||
|
|
||||||
|
expect(user).toEqual({ id: 'mockUserId', login: 'mockUserLogin' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the response is not ok', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.github.com/user',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'invalidAccessToken';
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||||
|
'Failed to fetch GitHub user: Unauthorized'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if user data is invalid', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.github.com/user',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'validAccessToken';
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||||
|
'Invalid GitHub user data received.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GitHubAdapter E2E Test', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
github: {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log in user using GitHub adapter successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://github.com/login/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.github.com/user',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
id: 'mockUserId',
|
||||||
|
login: 'mockUserLogin',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode' };
|
||||||
|
const user = await Parse.User.logInWith('github', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when GitHub returns invalid code', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://github.com/login/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Invalid code',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'invalidCode' };
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||||
|
'Failed to exchange code for token: Invalid code'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when GitHub returns invalid user data', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://github.com/login/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.github.com/user',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode' };
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||||
|
'Failed to fetch GitHub user: Unauthorized'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('e2e secure does not support insecure payload', async function () {
|
||||||
|
mockFetch();
|
||||||
|
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||||
|
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||||
|
'GitHub code is required.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('e2e insecure does support secure payload', async function () {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
github: {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
enableInsecureAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://github.com/login/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.github.com/user',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
id: 'mockUserId',
|
||||||
|
login: 'mockUserLogin',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode' };
|
||||||
|
const user = await Parse.User.logInWith('github', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
356
spec/Adapters/Auth/gpgames.spec.js
Normal file
356
spec/Adapters/Auth/gpgames.spec.js
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
const GooglePlayGamesServicesAdapter = require('../../../lib/Adapters/Auth/gpgames').default;
|
||||||
|
|
||||||
|
describe('GooglePlayGamesServicesAdapter', function () {
|
||||||
|
let adapter;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new GooglePlayGamesServicesAdapter.constructor();
|
||||||
|
adapter.clientId = 'validClientId';
|
||||||
|
adapter.clientSecret = 'validClientSecret';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAccessTokenFromCode', function () {
|
||||||
|
it('should fetch an access token successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://oauth2.googleapis.com/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const code = 'validCode';
|
||||||
|
const authData = { redirectUri: 'http://example.com' };
|
||||||
|
const token = await adapter.getAccessTokenFromCode(code, authData);
|
||||||
|
|
||||||
|
expect(token).toBe('mockAccessToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the response is not ok', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://oauth2.googleapis.com/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Bad Request',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const code = 'invalidCode';
|
||||||
|
const authData = { redirectUri: 'http://example.com' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
|
||||||
|
'Failed to exchange code for token: Bad Request'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the response contains an error', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://oauth2.googleapis.com/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
error: 'invalid_grant',
|
||||||
|
error_description: 'Code is invalid',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const code = 'invalidCode';
|
||||||
|
const authData = { redirectUri: 'http://example.com' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
|
||||||
|
'Code is invalid'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserFromAccessToken', function () {
|
||||||
|
it('should fetch user data successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
playerId: 'mockUserId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'validAccessToken';
|
||||||
|
const authData = { id: 'mockUserId' };
|
||||||
|
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||||
|
|
||||||
|
expect(user).toEqual({ id: 'mockUserId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the response is not ok', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'invalidAccessToken';
|
||||||
|
const authData = { id: 'mockUserId' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||||
|
'Failed to fetch Google Play Games Services user: Unauthorized'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if user data is invalid', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'validAccessToken';
|
||||||
|
const authData = { id: 'mockUserId' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||||
|
'Invalid Google Play Games Services user data received.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if playerId does not match the provided user ID', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
playerId: 'anotherUserId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'validAccessToken';
|
||||||
|
const authData = { id: 'mockUserId' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||||
|
'Invalid Google Play Games Services user data received.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GooglePlayGamesServicesAdapter E2E Test', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
gpgames: {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log in user successfully with valid code', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://oauth2.googleapis.com/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
playerId: 'mockUserId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
id: 'mockUserId',
|
||||||
|
redirectUri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = await Parse.User.logInWith('gpgames', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://oauth2.googleapis.com/token',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when the token exchange fails', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://oauth2.googleapis.com/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Invalid code',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'invalidCode',
|
||||||
|
redirectUri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||||
|
'Failed to exchange code for token: Invalid code'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://oauth2.googleapis.com/token',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when user data fetch fails', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://oauth2.googleapis.com/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
id: 'mockUserId',
|
||||||
|
redirectUri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||||
|
'Failed to fetch Google Play Games Services user: Unauthorized'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://oauth2.googleapis.com/token',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when user data is invalid', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://oauth2.googleapis.com/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
playerId: 'anotherUserId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
id: 'mockUserId',
|
||||||
|
redirectUri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||||
|
'Invalid Google Play Games Services user data received.'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://oauth2.googleapis.com/token',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when no code or access token is provided', async function () {
|
||||||
|
mockFetch();
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
id: 'mockUserId',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||||
|
'gpgames code is required.'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(global.fetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
258
spec/Adapters/Auth/instagram.spec.js
Normal file
258
spec/Adapters/Auth/instagram.spec.js
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
const InstagramAdapter = require('../../../lib/Adapters/Auth/instagram').default;
|
||||||
|
|
||||||
|
describe('InstagramAdapter', function () {
|
||||||
|
let adapter;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new InstagramAdapter.constructor();
|
||||||
|
adapter.clientId = 'validClientId';
|
||||||
|
adapter.clientSecret = 'validClientSecret';
|
||||||
|
adapter.redirectUri = 'https://example.com/callback';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAccessTokenFromCode', function () {
|
||||||
|
it('should fetch an access token successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.instagram.com/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode' };
|
||||||
|
const token = await adapter.getAccessTokenFromCode(authData);
|
||||||
|
|
||||||
|
expect(token).toBe('mockAccessToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the response contains an error', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.instagram.com/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
error: 'invalid_grant',
|
||||||
|
error_description: 'Code is invalid',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'invalidCode' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||||
|
'Code is invalid'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserFromAccessToken', function () {
|
||||||
|
it('should fetch user data successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
id: 'mockUserId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'mockAccessToken';
|
||||||
|
const authData = { id: 'mockUserId' };
|
||||||
|
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||||
|
|
||||||
|
expect(user).toEqual({ id: 'mockUserId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if user ID does not match authData', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
id: 'differentUserId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'mockAccessToken';
|
||||||
|
const authData = { id: 'mockUserId' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||||
|
'Instagram auth is invalid for this user.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('InstagramAdapter E2E Test', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
instagram: {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
redirectUri: 'https://example.com/callback',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log in user successfully with valid code', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.instagram.com/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
id: 'mockUserId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
id: 'mockUserId',
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = await Parse.User.logInWith('instagram', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when access token exchange fails', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.instagram.com/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Invalid code',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'invalidCode',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
|
||||||
|
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when user data fetch fails', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.instagram.com/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
id: 'mockUserId',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
|
||||||
|
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when user data is invalid', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.instagram.com/oauth/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
id: 'differentUserId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
id: 'mockUserId',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
|
||||||
|
'Instagram auth is invalid for this user.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when no code or access token is provided', async function () {
|
||||||
|
mockFetch();
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
id: 'mockUserId',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
|
||||||
|
'Instagram code is required.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
309
spec/Adapters/Auth/line.spec.js
Normal file
309
spec/Adapters/Auth/line.spec.js
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
const LineAdapter = require('../../../lib/Adapters/Auth/line').default;
|
||||||
|
describe('LineAdapter', function () {
|
||||||
|
let adapter;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new LineAdapter.constructor();
|
||||||
|
adapter.clientId = 'validClientId';
|
||||||
|
adapter.clientSecret = 'validClientSecret';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAccessTokenFromCode', function () {
|
||||||
|
it('should throw an error if code is missing in authData', async function () {
|
||||||
|
const authData = { redirect_uri: 'http://example.com' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||||
|
'Line auth is invalid for this user.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch an access token successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
redirect_uri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = await adapter.getAccessTokenFromCode(authData);
|
||||||
|
|
||||||
|
expect(token).toBe('mockAccessToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if response is not ok', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Bad Request',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'invalidCode',
|
||||||
|
redirect_uri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||||
|
'Failed to exchange code for token: Bad Request'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if response contains an error object', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
error: 'invalid_grant',
|
||||||
|
error_description: 'Code is invalid',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'invalidCode',
|
||||||
|
redirect_uri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||||
|
'Code is invalid'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserFromAccessToken', function () {
|
||||||
|
it('should fetch user data successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/v2/profile',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
userId: 'mockUserId',
|
||||||
|
displayName: 'mockDisplayName',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'validAccessToken';
|
||||||
|
const user = await adapter.getUserFromAccessToken(accessToken);
|
||||||
|
|
||||||
|
expect(user).toEqual({
|
||||||
|
userId: 'mockUserId',
|
||||||
|
displayName: 'mockDisplayName',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if response is not ok', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/v2/profile',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'invalidAccessToken';
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||||
|
'Failed to fetch Line user: Unauthorized'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if user data is invalid', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/v2/profile',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accessToken = 'validAccessToken';
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||||
|
'Invalid Line user data received.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('LineAdapter E2E Test', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
line: {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log in user successfully with valid code', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/v2/profile',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
userId: 'mockUserId',
|
||||||
|
displayName: 'mockDisplayName',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
redirect_uri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = await Parse.User.logInWith('line', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when token exchange fails', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Invalid code',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'invalidCode',
|
||||||
|
redirect_uri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||||
|
'Failed to exchange code for token: Invalid code'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when user data fetch fails', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/v2/profile',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
redirect_uri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||||
|
'Failed to fetch Line user: Unauthorized'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when user data is invalid', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.line.me/v2/profile',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
redirect_uri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||||
|
'Invalid Line user data received.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when no code is provided', async function () {
|
||||||
|
mockFetch();
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
redirect_uri: 'http://example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||||
|
'Line code is required.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
312
spec/Adapters/Auth/linkedIn.spec.js
Normal file
312
spec/Adapters/Auth/linkedIn.spec.js
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
|
||||||
|
const LinkedInAdapter = require('../../../lib/Adapters/Auth/linkedin').default;
|
||||||
|
describe('LinkedInAdapter', function () {
|
||||||
|
let adapter;
|
||||||
|
const validOptions = {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
enableInsecureAuth: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new LinkedInAdapter.constructor();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test configuration errors', function () {
|
||||||
|
it('should throw error for missing options', function () {
|
||||||
|
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||||
|
|
||||||
|
for (const options of invalidOptions) {
|
||||||
|
expect(() => {
|
||||||
|
adapter.validateOptions(options);
|
||||||
|
}).toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate options successfully with valid parameters', function () {
|
||||||
|
expect(() => {
|
||||||
|
adapter.validateOptions(validOptions);
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||||
|
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||||
|
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test beforeFind', function () {
|
||||||
|
it('should throw error for invalid payload', async function () {
|
||||||
|
adapter.enableInsecureAuth = true;
|
||||||
|
|
||||||
|
const payloads = [{}, { access_token: null }];
|
||||||
|
|
||||||
|
for (const payload of payloads) {
|
||||||
|
await expectAsync(adapter.beforeFind(payload)).toBeRejectedWith(
|
||||||
|
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn auth is invalid for this user.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process secure payload and set auth data', async function () {
|
||||||
|
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(
|
||||||
|
Promise.resolve('validToken')
|
||||||
|
);
|
||||||
|
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||||
|
Promise.resolve({ id: 'validUserId' })
|
||||||
|
);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com', is_mobile_sdk: false };
|
||||||
|
|
||||||
|
await adapter.beforeFind(authData);
|
||||||
|
|
||||||
|
expect(authData.access_token).toBe('validToken');
|
||||||
|
expect(authData.id).toBe('validUserId');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate insecure auth and match user id', async function () {
|
||||||
|
adapter.enableInsecureAuth = true;
|
||||||
|
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||||
|
Promise.resolve({ id: 'validUserId' })
|
||||||
|
);
|
||||||
|
|
||||||
|
const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
|
||||||
|
|
||||||
|
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if insecure auth user id does not match', async function () {
|
||||||
|
adapter.enableInsecureAuth = true;
|
||||||
|
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||||
|
Promise.resolve({ id: 'invalidUserId' })
|
||||||
|
);
|
||||||
|
|
||||||
|
const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
|
||||||
|
|
||||||
|
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||||
|
new Error('LinkedIn auth is invalid for this user.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test getUserFromAccessToken', function () {
|
||||||
|
it('should fetch user successfully', async function () {
|
||||||
|
global.fetch = jasmine.createSpy().and.returnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await adapter.getUserFromAccessToken('validToken', false);
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('https://api.linkedin.com/v2/me', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer validToken`,
|
||||||
|
'x-li-format': 'json',
|
||||||
|
'x-li-src': undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid response', async function () {
|
||||||
|
global.fetch = jasmine.createSpy().and.returnValue(
|
||||||
|
Promise.resolve({ ok: false })
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken('invalidToken', false)).toBeRejectedWith(
|
||||||
|
new Error('LinkedIn API request failed.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test getAccessTokenFromCode', function () {
|
||||||
|
it('should fetch token successfully', async function () {
|
||||||
|
global.fetch = jasmine.createSpy().and.returnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokenResponse = await adapter.getAccessTokenFromCode('validCode', 'http://example.com');
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('https://www.linkedin.com/oauth/v2/accessToken', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: jasmine.any(URLSearchParams),
|
||||||
|
});
|
||||||
|
expect(tokenResponse).toEqual('validToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid response', async function () {
|
||||||
|
global.fetch = jasmine.createSpy().and.returnValue(
|
||||||
|
Promise.resolve({ ok: false })
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
adapter.getAccessTokenFromCode('invalidCode', 'http://example.com')
|
||||||
|
).toBeRejectedWith(new Error('LinkedIn API request failed.'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test validate methods', function () {
|
||||||
|
const authData = { id: 'validUserId', access_token: 'validToken' };
|
||||||
|
|
||||||
|
it('validateLogin should return user id', function () {
|
||||||
|
const result = adapter.validateLogin(authData);
|
||||||
|
expect(result).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validateSetUp should return user id', function () {
|
||||||
|
const result = adapter.validateSetUp(authData);
|
||||||
|
expect(result).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validateUpdate should return user id', function () {
|
||||||
|
const result = adapter.validateUpdate(authData);
|
||||||
|
expect(result).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('afterFind should return user id', function () {
|
||||||
|
const result = adapter.afterFind(authData);
|
||||||
|
expect(result).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('LinkedInAdapter E2E Test', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
linkedin: {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log in user using LinkedIn adapter successfully (secure)', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.linkedin.com/v2/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
id: 'mockUserId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
|
||||||
|
const user = await Parse.User.logInWith('linkedin', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.linkedin.com/v2/me',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when LinkedIn returns invalid user data', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
access_token: 'mockAccessToken123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.linkedin.com/v2/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
|
||||||
|
'LinkedIn API request failed.'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.linkedin.com/v2/me',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('secure does not support insecure payload if not enabled', async function () {
|
||||||
|
mockFetch();
|
||||||
|
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||||
|
await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
|
||||||
|
'LinkedIn code is required.'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(global.fetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('insecure mode supports insecure payload if enabled', async function () {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
linkedin: {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
enableInsecureAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.linkedin.com/v2/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
id: 'mockUserId',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||||
|
const user = await Parse.User.logInWith('linkedin', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.linkedin.com/v2/me',
|
||||||
|
jasmine.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
307
spec/Adapters/Auth/microsoft.spec.js
Normal file
307
spec/Adapters/Auth/microsoft.spec.js
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
const MicrosoftAdapter = require('../../../lib/Adapters/Auth/microsoft').default;
|
||||||
|
|
||||||
|
describe('MicrosoftAdapter', function () {
|
||||||
|
let adapter;
|
||||||
|
const validOptions = {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
enableInsecureAuth: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new MicrosoftAdapter.constructor();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test configuration errors', function () {
|
||||||
|
it('should throw error for missing options', function () {
|
||||||
|
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||||
|
|
||||||
|
for (const options of invalidOptions) {
|
||||||
|
expect(() => {
|
||||||
|
adapter.validateOptions(options);
|
||||||
|
}).toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate options successfully with valid parameters', function () {
|
||||||
|
expect(() => {
|
||||||
|
adapter.validateOptions(validOptions);
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||||
|
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||||
|
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test getUserFromAccessToken', function () {
|
||||||
|
it('should fetch user successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.microsoft.com/v1.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const user = await adapter.getUserFromAccessToken('validToken');
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('https://graph.microsoft.com/v1.0/me', {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer validToken',
|
||||||
|
},
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
expect(user).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid response', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.microsoft.com/v1.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: { ok: false },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken('invalidToken')).toBeRejectedWith(
|
||||||
|
new Error('Microsoft API request failed.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test getAccessTokenFromCode', function () {
|
||||||
|
it('should fetch token successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||||
|
const token = await adapter.getAccessTokenFromCode(authData);
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: jasmine.any(URLSearchParams),
|
||||||
|
});
|
||||||
|
expect(token).toEqual('validToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid response', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: { ok: false },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com' };
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||||
|
new Error('Microsoft API request failed.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test secure authentication flow', function () {
|
||||||
|
it('should exchange code for access token and fetch user data', async function () {
|
||||||
|
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||||
|
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(Promise.resolve({ id: 'validUserId' }));
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||||
|
await adapter.beforeFind(authData);
|
||||||
|
|
||||||
|
expect(authData.access_token).toBe('validToken');
|
||||||
|
expect(authData.id).toBe('validUserId');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if user data cannot be fetched', async function () {
|
||||||
|
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||||
|
spyOn(adapter, 'getUserFromAccessToken').and.throwError('Microsoft API request failed.');
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||||
|
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||||
|
new Error('Microsoft API request failed.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test insecure authentication flow', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter.enableInsecureAuth = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate insecure auth and match user id', async function () {
|
||||||
|
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||||
|
Promise.resolve({ id: 'validUserId' })
|
||||||
|
);
|
||||||
|
|
||||||
|
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||||
|
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if insecure auth user id does not match', async function () {
|
||||||
|
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||||
|
Promise.resolve({ id: 'invalidUserId' })
|
||||||
|
);
|
||||||
|
|
||||||
|
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||||
|
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||||
|
new Error('Microsoft auth is invalid for this user.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MicrosoftAdapter E2E Tests', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Simulate reconfiguring the server with Microsoft auth options
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
microsoft: {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
enableInsecureAuth: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should authenticate user successfully using MicrosoftAdapter', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://graph.microsoft.com/v1.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ id: 'user123' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid code error gracefully', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: { ok: false, statusText: 'Invalid code' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||||
|
'Microsoft API request failed.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when fetching user data fails', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://graph.microsoft.com/v1.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: { ok: false, statusText: 'Unauthorized' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||||
|
'Microsoft API request failed.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow insecure auth when enabled', async () => {
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.microsoft.com/v1.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
id: 'user123',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
microsoft: {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
enableInsecureAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||||
|
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject insecure auth when user id does not match', async () => {
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.microsoft.com/v1.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
id: 'incorrectUser',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
microsoft: {
|
||||||
|
clientId: 'validClientId',
|
||||||
|
clientSecret: 'validClientSecret',
|
||||||
|
enableInsecureAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||||
|
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||||
|
'Microsoft auth is invalid for this user.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
305
spec/Adapters/Auth/oauth2.spec.js
Normal file
305
spec/Adapters/Auth/oauth2.spec.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
const OAuth2Adapter = require('../../../lib/Adapters/Auth/oauth2').default;
|
||||||
|
|
||||||
|
describe('OAuth2Adapter', () => {
|
||||||
|
let adapter;
|
||||||
|
|
||||||
|
const validOptions = {
|
||||||
|
tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
|
||||||
|
useridField: 'sub',
|
||||||
|
appidField: 'aud',
|
||||||
|
appIds: ['valid-app-id'],
|
||||||
|
authorizationHeader: 'Bearer validAuthToken',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
adapter = new OAuth2Adapter.constructor();
|
||||||
|
adapter.validateOptions(validOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateAppId', () => {
|
||||||
|
it('should validate app ID successfully', async () => {
|
||||||
|
const authData = { access_token: 'validAccessToken' };
|
||||||
|
const mockResponse = {
|
||||||
|
[validOptions.appidField]: 'valid-app-id',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
adapter.validateAppId(validOptions.appIds, authData, validOptions)
|
||||||
|
).toBeResolved();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if app ID is invalid', async () => {
|
||||||
|
const authData = { access_token: 'validAccessToken' };
|
||||||
|
const mockResponse = {
|
||||||
|
[validOptions.appidField]: 'invalid-app-id',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
adapter.validateAppId(validOptions.appIds, authData, validOptions)
|
||||||
|
).toBeRejectedWithError('OAuth2: Invalid app ID.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateAuthData', () => {
|
||||||
|
it('should validate auth data successfully', async () => {
|
||||||
|
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||||
|
const mockResponse = {
|
||||||
|
active: true,
|
||||||
|
[validOptions.useridField]: 'user-id',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
adapter.validateAuthData(authData, null, validOptions)
|
||||||
|
).toBeResolvedTo({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the token is inactive', async () => {
|
||||||
|
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||||
|
const mockResponse = { active: false };
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
adapter.validateAuthData(authData, null, validOptions)
|
||||||
|
).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if user ID does not match', async () => {
|
||||||
|
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||||
|
const mockResponse = {
|
||||||
|
active: true,
|
||||||
|
[validOptions.useridField]: 'different-user-id',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
adapter.validateAuthData(authData, null, validOptions)
|
||||||
|
).toBeRejectedWithError('OAuth2 access token is invalid for this user.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('requestTokenInfo', () => {
|
||||||
|
it('should fetch token info successfully', async () => {
|
||||||
|
const mockResponse = { active: true };
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await adapter.requestTokenInfo(
|
||||||
|
'validAccessToken',
|
||||||
|
validOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the introspection endpoint URL is missing', async () => {
|
||||||
|
const options = { ...validOptions, tokenIntrospectionEndpointUrl: null };
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => adapter.validateOptions(options)
|
||||||
|
).toThrow(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the response is not ok', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Bad Request',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
adapter.requestTokenInfo('invalidAccessToken')
|
||||||
|
).toBeRejectedWithError('OAuth2 token introspection request failed.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OAuth2Adapter E2E Tests', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Simulate reconfiguring the server with OAuth2 auth options
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
mockOauth: {
|
||||||
|
tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
|
||||||
|
useridField: 'sub',
|
||||||
|
appidField: 'aud',
|
||||||
|
appIds: ['valid-app-id'],
|
||||||
|
authorizationHeader: 'Bearer validAuthToken',
|
||||||
|
oauth2: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate and authenticate user successfully', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://provider.com/introspect',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
active: true,
|
||||||
|
sub: 'user123',
|
||||||
|
aud: 'valid-app-id',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||||
|
const user = await Parse.User.logInWith('mockOauth', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
expect(user.get('authData').mockOauth.id).toEqual('user123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject authentication for inactive token', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://provider.com/introspect',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ active: false, aud: ['valid-app-id'] }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { access_token: 'inactiveToken', id: 'user123' };
|
||||||
|
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||||
|
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject authentication for mismatched user ID', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://provider.com/introspect',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
active: true,
|
||||||
|
sub: 'different-user',
|
||||||
|
aud: 'valid-app-id',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||||
|
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||||
|
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject authentication for invalid app ID', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://provider.com/introspect',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
active: true,
|
||||||
|
sub: 'user123',
|
||||||
|
aud: 'invalid-app-id',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||||
|
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWithError(
|
||||||
|
'OAuth2: Invalid app ID.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when token introspection endpoint is missing', async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
mockOauth: {
|
||||||
|
tokenIntrospectionEndpointUrl: null,
|
||||||
|
useridField: 'sub',
|
||||||
|
appidField: 'aud',
|
||||||
|
appIds: ['valid-app-id'],
|
||||||
|
authorizationHeader: 'Bearer validAuthToken',
|
||||||
|
oauth2: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||||
|
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||||
|
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
252
spec/Adapters/Auth/qq.spec.js
Normal file
252
spec/Adapters/Auth/qq.spec.js
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
const QqAdapter = require('../../../lib/Adapters/Auth/qq').default;
|
||||||
|
|
||||||
|
describe('QqAdapter', () => {
|
||||||
|
let adapter;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
adapter = new QqAdapter.constructor();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserFromAccessToken', () => {
|
||||||
|
it('should fetch user data successfully', async () => {
|
||||||
|
const mockResponse = `callback({"client_id":"validAppId","openid":"user123"})`;
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
text: () => Promise.resolve(mockResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await adapter.getUserFromAccessToken('validAccessToken');
|
||||||
|
|
||||||
|
expect(result).toEqual({ client_id: 'validAppId', openid: 'user123' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the API request fails', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
adapter.getUserFromAccessToken('invalidAccessToken')
|
||||||
|
).toBeRejectedWithError('qq API request failed.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAccessTokenFromCode', () => {
|
||||||
|
it('should fetch access token successfully', async () => {
|
||||||
|
const mockResponse = `callback({"access_token":"validAccessToken","expires_in":3600,"refresh_token":"refreshToken"})`;
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/token',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
text: () => Promise.resolve(mockResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await adapter.getAccessTokenFromCode({
|
||||||
|
code: 'validCode',
|
||||||
|
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('validAccessToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the API request fails', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/token',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Bad Request',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
adapter.getAccessTokenFromCode({
|
||||||
|
code: 'invalidCode',
|
||||||
|
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||||
|
})
|
||||||
|
).toBeRejectedWithError('qq API request failed.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseResponseData', () => {
|
||||||
|
it('should parse valid callback response data', () => {
|
||||||
|
const response = `callback({"key":"value"})`;
|
||||||
|
const result = adapter.parseResponseData(response);
|
||||||
|
|
||||||
|
expect(result).toEqual({ key: 'value' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the response data is invalid', () => {
|
||||||
|
const response = 'invalid response';
|
||||||
|
|
||||||
|
expect(() => adapter.parseResponseData(response)).toThrowError(
|
||||||
|
'qq auth is invalid for this user.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('QqAdapter E2E Test', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
qq: {
|
||||||
|
clientId: 'validAppId',
|
||||||
|
clientSecret: 'validAppSecret',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log in user using Qq adapter successfully', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/token',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
text: () =>
|
||||||
|
Promise.resolve(
|
||||||
|
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
text: () =>
|
||||||
|
Promise.resolve(
|
||||||
|
`callback({"client_id":"validAppId","openid":"user123"})`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||||
|
const user = await Parse.User.logInWith('qq', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when Qq returns invalid code', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/token',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Invalid code',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'invalidCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||||
|
'qq API request failed.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when Qq returns invalid user data', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/token',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
text: () =>
|
||||||
|
Promise.resolve(
|
||||||
|
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||||
|
'qq API request failed.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('e2e secure does not support insecure payload', async () => {
|
||||||
|
mockFetch();
|
||||||
|
const authData = { id: 'mockUserId', access_token: 'mockAccessToken' };
|
||||||
|
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||||
|
'qq code is required.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('e2e insecure does support secure payload', async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
qq: {
|
||||||
|
appId: 'validAppId',
|
||||||
|
appSecret: 'validAppSecret',
|
||||||
|
enableInsecureAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/token',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
text: () =>
|
||||||
|
Promise.resolve(
|
||||||
|
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://graph.qq.com/oauth2.0/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
text: () =>
|
||||||
|
Promise.resolve(
|
||||||
|
`callback({"client_id":"validAppId","openid":"user123"})`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||||
|
const user = await Parse.User.logInWith('qq', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
113
spec/Adapters/Auth/spotify.spec.js
Normal file
113
spec/Adapters/Auth/spotify.spec.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
const SpotifyAdapter = require('../../../lib/Adapters/Auth/spotify').default;
|
||||||
|
|
||||||
|
describe('SpotifyAdapter', () => {
|
||||||
|
let adapter;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
adapter = new SpotifyAdapter.constructor();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserFromAccessToken', () => {
|
||||||
|
it('should fetch user data successfully', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
id: 'spotifyUser123',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.spotify.com/v1/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await adapter.getUserFromAccessToken('validAccessToken');
|
||||||
|
|
||||||
|
expect(result).toEqual({ id: 'spotifyUser123' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the API request fails', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.spotify.com/v1/me',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken')).toBeRejectedWithError(
|
||||||
|
'Spotify API request failed.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAccessTokenFromCode', () => {
|
||||||
|
it('should fetch access token successfully', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
access_token: 'validAccessToken',
|
||||||
|
expires_in: 3600,
|
||||||
|
refresh_token: 'refreshToken',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://accounts.spotify.com/api/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'validCode',
|
||||||
|
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||||
|
code_verifier: 'validCodeVerifier',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await adapter.getAccessTokenFromCode(authData);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if authData is missing required fields', async () => {
|
||||||
|
const authData = {
|
||||||
|
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||||
|
'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the API request fails', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://accounts.spotify.com/api/token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Bad Request',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
code: 'invalidCode',
|
||||||
|
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||||
|
code_verifier: 'invalidCodeVerifier',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||||
|
'Spotify API request failed.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
120
spec/Adapters/Auth/twitter.spec.js
Normal file
120
spec/Adapters/Auth/twitter.spec.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
const TwitterAuthAdapter = require('../../../lib/Adapters/Auth/twitter').default;
|
||||||
|
|
||||||
|
describe('TwitterAuthAdapter', function () {
|
||||||
|
let adapter;
|
||||||
|
const validOptions = {
|
||||||
|
consumer_key: 'validConsumerKey',
|
||||||
|
consumer_secret: 'validConsumerSecret',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new TwitterAuthAdapter.constructor();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test configuration errors', function () {
|
||||||
|
it('should throw an error when options are missing', function () {
|
||||||
|
expect(() => adapter.validateOptions()).toThrowError('Twitter auth options are required.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when consumer_key and consumer_secret are missing for secure auth', function () {
|
||||||
|
const options = { enableInsecureAuth: false };
|
||||||
|
expect(() => adapter.validateOptions(options)).toThrowError(
|
||||||
|
'Consumer key and secret are required for secure Twitter auth.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw an error when valid options are provided', function () {
|
||||||
|
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validate Insecure Auth', function () {
|
||||||
|
it('should throw an error if oauth_token or oauth_token_secret are missing', async function () {
|
||||||
|
const authData = { oauth_token: 'validToken' }; // Missing oauth_token_secret
|
||||||
|
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
|
||||||
|
'Twitter insecure auth requires oauth_token and oauth_token_secret.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate insecure auth successfully when data matches', async function () {
|
||||||
|
spyOn(adapter, 'request').and.returnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
id: 'validUserId',
|
||||||
|
oauth_token: 'validToken',
|
||||||
|
oauth_token_secret: 'validSecret',
|
||||||
|
};
|
||||||
|
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeResolved();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when user ID does not match', async function () {
|
||||||
|
spyOn(adapter, 'request').and.returnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
json: () => Promise.resolve({ id: 'invalidUserId' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
id: 'validUserId',
|
||||||
|
oauth_token: 'validToken',
|
||||||
|
oauth_token_secret: 'validSecret',
|
||||||
|
};
|
||||||
|
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
|
||||||
|
'Twitter auth is invalid for this user.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('End-to-End Tests', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
twitter: validOptions,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should authenticate user successfully using validateAuthData', async function () {
|
||||||
|
spyOn(adapter, 'exchangeAccessToken').and.returnValue(
|
||||||
|
Promise.resolve({ oauth_token: 'validToken', user_id: 'validUserId' })
|
||||||
|
);
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
oauth_token: 'validToken',
|
||||||
|
oauth_verifier: 'validVerifier',
|
||||||
|
};
|
||||||
|
await expectAsync(adapter.validateAuthData(authData, validOptions)).toBeResolved();
|
||||||
|
expect(authData.id).toBe('validUserId');
|
||||||
|
expect(authData.auth_token).toBe('validToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple configurations and validate successfully', async function () {
|
||||||
|
const authData = {
|
||||||
|
consumer_key: 'validConsumerKey',
|
||||||
|
oauth_token: 'validToken',
|
||||||
|
oauth_token_secret: 'validSecret',
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionsArray = [
|
||||||
|
{ consumer_key: 'invalidKey', consumer_secret: 'invalidSecret' },
|
||||||
|
validOptions,
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedOption = adapter.handleMultipleConfigurations(authData, optionsArray);
|
||||||
|
expect(selectedOption).toEqual(validOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when no matching configuration is found', function () {
|
||||||
|
const authData = { consumer_key: 'missingKey' };
|
||||||
|
const optionsArray = [validOptions];
|
||||||
|
|
||||||
|
expect(() => adapter.handleMultipleConfigurations(authData, optionsArray)).toThrowError(
|
||||||
|
'Twitter auth is invalid for this user.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
234
spec/Adapters/Auth/wechat.spec.js
Normal file
234
spec/Adapters/Auth/wechat.spec.js
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
const WeChatAdapter = require('../../../lib/Adapters/Auth/wechat').default;
|
||||||
|
|
||||||
|
describe('WeChatAdapter', function () {
|
||||||
|
let adapter;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new WeChatAdapter.constructor();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test getUserFromAccessToken', function () {
|
||||||
|
it('should fetch user successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ errcode: 0, id: 'validUserId' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const user = await adapter.getUserFromAccessToken('validToken', { id: 'validOpenId' });
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId'
|
||||||
|
);
|
||||||
|
expect(user).toEqual({ errcode: 0, id: 'validUserId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid response', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/auth?access_token=invalidToken&openid=undefined',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken('invalidToken', 'invalidOpenId')).toBeRejectedWith(
|
||||||
|
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test getAccessTokenFromCode', function () {
|
||||||
|
it('should fetch access token successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ access_token: 'validToken', errcode: 0 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
|
||||||
|
const authData = { code: 'validCode' };
|
||||||
|
const token = await adapter.getAccessTokenFromCode(authData);
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code'
|
||||||
|
);
|
||||||
|
expect(token).toEqual('validToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid response', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
|
||||||
|
|
||||||
|
const authData = { code: 'invalidCode' };
|
||||||
|
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||||
|
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WeChatAdapter E2E Tests', function () {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
wechat: {
|
||||||
|
clientId: 'validAppId',
|
||||||
|
clientSecret: 'validAppSecret',
|
||||||
|
enableInsecureAuth: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should authenticate user successfully using WeChatAdapter', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
const user = await Parse.User.logInWith('wechat', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid code error gracefully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||||
|
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when fetching user data fails', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
|
||||||
|
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||||
|
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow insecure auth when enabled', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
wechat: {
|
||||||
|
appId: 'validAppId',
|
||||||
|
appSecret: 'validAppSecret',
|
||||||
|
enableInsecureAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||||
|
const user = await Parse.User.logInWith('wechat', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject insecure auth when user id does not match', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=incorrectUserId',
|
||||||
|
method: 'GET',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ errcode: 0, id: 'incorrectUser' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
wechat: {
|
||||||
|
appId: 'validAppId',
|
||||||
|
appSecret: 'validAppSecret',
|
||||||
|
enableInsecureAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||||
|
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||||
|
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
204
spec/Adapters/Auth/weibo.spec.js
Normal file
204
spec/Adapters/Auth/weibo.spec.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
const WeiboAdapter = require('../../../lib/Adapters/Auth/weibo').default;
|
||||||
|
|
||||||
|
describe('WeiboAdapter', function () {
|
||||||
|
let adapter;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
adapter = new WeiboAdapter.constructor();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test configuration errors', function () {
|
||||||
|
it('should throw error if code or redirect_uri is missing', async function () {
|
||||||
|
const invalidAuthData = [
|
||||||
|
{},
|
||||||
|
{ code: 'validCode' },
|
||||||
|
{ redirect_uri: 'http://example.com/callback' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const authData of invalidAuthData) {
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
message: 'Weibo auth requires code and redirect_uri to be sent.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test getUserFromAccessToken', function () {
|
||||||
|
it('should fetch user successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ uid: 'validUserId' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { id: 'validUserId' };
|
||||||
|
const user = await adapter.getUserFromAccessToken('validToken', authData);
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.weibo.com/oauth2/get_token_info',
|
||||||
|
jasmine.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(user).toEqual({ id: 'validUserId' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid response', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
json: () => Promise.resolve({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { id: 'invalidUserId' };
|
||||||
|
await expectAsync(adapter.getUserFromAccessToken('invalidToken', authData)).toBeRejectedWith(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
message: 'Weibo auth is invalid for this user.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test getAccessTokenFromCode', function () {
|
||||||
|
it('should fetch access token successfully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weibo.com/oauth2/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ access_token: 'validToken', uid: 'validUserId' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
const token = await adapter.getAccessTokenFromCode(authData);
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.weibo.com/oauth2/access_token',
|
||||||
|
jasmine.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(token).toEqual('validToken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid response', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weibo.com/oauth2/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
json: () => Promise.resolve({ errcode: 40029 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
message: 'Weibo auth is invalid for this user.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WeiboAdapter E2E Tests', function () {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
weibo: {
|
||||||
|
clientId: 'validAppId',
|
||||||
|
clientSecret: 'validAppSecret',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should authenticate user successfully using WeiboAdapter', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weibo.com/oauth2/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ uid: 'user123' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
const user = await Parse.User.logInWith('weibo', { authData });
|
||||||
|
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid code error gracefully', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weibo.com/oauth2/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
json: () => Promise.resolve({ errcode: 40029 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
|
||||||
|
jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error when fetching user data fails', async function () {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: 'https://api.weibo.com/oauth2/access_token',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||||
|
method: 'POST',
|
||||||
|
response: {
|
||||||
|
ok: false,
|
||||||
|
json: () => Promise.resolve({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||||
|
await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
|
||||||
|
jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,99 +3,8 @@ const Config = require('../lib/Config');
|
|||||||
const defaultColumns = require('../lib/Controllers/SchemaController').defaultColumns;
|
const defaultColumns = require('../lib/Controllers/SchemaController').defaultColumns;
|
||||||
const authenticationLoader = require('../lib/Adapters/Auth');
|
const authenticationLoader = require('../lib/Adapters/Auth');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const responses = {
|
|
||||||
gpgames: { playerId: 'userId' },
|
|
||||||
instagram: { id: 'userId' },
|
|
||||||
janrainengage: { stat: 'ok', profile: { identifier: 'userId' } },
|
|
||||||
janraincapture: { stat: 'ok', result: 'userId' },
|
|
||||||
line: { userId: 'userId' },
|
|
||||||
vkontakte: { response: [{ id: 'userId' }] },
|
|
||||||
google: { sub: 'userId' },
|
|
||||||
wechat: { errcode: 0 },
|
|
||||||
weibo: { uid: 'userId' },
|
|
||||||
qq: 'callback( {"openid":"userId"} );', // yes it's like that, run eval in the client :P
|
|
||||||
phantauth: { sub: 'userId' },
|
|
||||||
microsoft: { id: 'userId', mail: 'userMail' },
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('AuthenticationProviders', function () {
|
describe('AuthenticationProviders', function () {
|
||||||
[
|
|
||||||
'apple',
|
|
||||||
'gcenter',
|
|
||||||
'gpgames',
|
|
||||||
'facebook',
|
|
||||||
'github',
|
|
||||||
'instagram',
|
|
||||||
'google',
|
|
||||||
'linkedin',
|
|
||||||
'meetup',
|
|
||||||
'twitter',
|
|
||||||
'janrainengage',
|
|
||||||
'janraincapture',
|
|
||||||
'line',
|
|
||||||
'vkontakte',
|
|
||||||
'qq',
|
|
||||||
'spotify',
|
|
||||||
'wechat',
|
|
||||||
'weibo',
|
|
||||||
'phantauth',
|
|
||||||
'microsoft',
|
|
||||||
'keycloak',
|
|
||||||
].map(function (providerName) {
|
|
||||||
it('Should validate structure of ' + providerName, done => {
|
|
||||||
const provider = require('../lib/Adapters/Auth/' + providerName);
|
|
||||||
jequal(typeof provider.validateAuthData, 'function');
|
|
||||||
jequal(typeof provider.validateAppId, 'function');
|
|
||||||
const validateAuthDataPromise = provider.validateAuthData({}, {});
|
|
||||||
const validateAppIdPromise = provider.validateAppId('app', 'key', {});
|
|
||||||
jequal(validateAuthDataPromise.constructor, Promise.prototype.constructor);
|
|
||||||
jequal(validateAppIdPromise.constructor, Promise.prototype.constructor);
|
|
||||||
validateAuthDataPromise.then(
|
|
||||||
() => {},
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
validateAppIdPromise.then(
|
|
||||||
() => {},
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should provide the right responses for adapter ${providerName}`, async () => {
|
|
||||||
const noResponse = ['twitter', 'apple', 'gcenter', 'google', 'keycloak'];
|
|
||||||
if (noResponse.includes(providerName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(options => {
|
|
||||||
if (
|
|
||||||
options ===
|
|
||||||
'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.123&grant_type=client_credentials' ||
|
|
||||||
options ===
|
|
||||||
'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.124&grant_type=client_credentials'
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
access_token: 'access_token',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return Promise.resolve(responses[providerName] || { id: 'userId' });
|
|
||||||
});
|
|
||||||
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'request').and.callFake(() => {
|
|
||||||
return Promise.resolve(responses[providerName] || { id: 'userId' });
|
|
||||||
});
|
|
||||||
const provider = require('../lib/Adapters/Auth/' + providerName);
|
|
||||||
let params = {};
|
|
||||||
if (providerName === 'vkontakte') {
|
|
||||||
params = {
|
|
||||||
appIds: 'appId',
|
|
||||||
appSecret: 'appSecret',
|
|
||||||
};
|
|
||||||
await provider.validateAuthData({ id: 'userId' }, params);
|
|
||||||
params.appVersion = '5.123';
|
|
||||||
}
|
|
||||||
await provider.validateAuthData({ id: 'userId' }, params);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const getMockMyOauthProvider = function () {
|
const getMockMyOauthProvider = function () {
|
||||||
return {
|
return {
|
||||||
authData: {
|
authData: {
|
||||||
@@ -568,46 +477,6 @@ describe('AuthenticationProviders', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('instagram auth adapter', () => {
|
|
||||||
const instagram = require('../lib/Adapters/Auth/instagram');
|
|
||||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
|
||||||
|
|
||||||
it('should use default api', async () => {
|
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ data: { id: 'userId' } });
|
|
||||||
});
|
|
||||||
await instagram.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
|
|
||||||
expect(httpsRequest.get).toHaveBeenCalledWith(
|
|
||||||
'https://graph.instagram.com/me?fields=id&access_token=the_token'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('response object without data child', async () => {
|
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ id: 'userId' });
|
|
||||||
});
|
|
||||||
await instagram.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
|
|
||||||
expect(httpsRequest.get).toHaveBeenCalledWith(
|
|
||||||
'https://graph.instagram.com/me?fields=id&access_token=the_token'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('should pass in api url', async () => {
|
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ data: { id: 'userId' } });
|
|
||||||
});
|
|
||||||
await instagram.validateAuthData(
|
|
||||||
{
|
|
||||||
id: 'userId',
|
|
||||||
access_token: 'the_token',
|
|
||||||
apiURL: 'https://new-api.instagram.com/v1/',
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
expect(httpsRequest.get).toHaveBeenCalledWith(
|
|
||||||
'https://new-api.instagram.com/v1/me?fields=id&access_token=the_token'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('google auth adapter', () => {
|
describe('google auth adapter', () => {
|
||||||
const google = require('../lib/Adapters/Auth/google');
|
const google = require('../lib/Adapters/Auth/google');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
@@ -730,35 +599,6 @@ describe('google auth adapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('google play games service auth', () => {
|
|
||||||
const gpgames = require('../lib/Adapters/Auth/gpgames');
|
|
||||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
|
||||||
|
|
||||||
it('validateAuthData should pass validation', async () => {
|
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ playerId: 'userId' });
|
|
||||||
});
|
|
||||||
await gpgames.validateAuthData({
|
|
||||||
id: 'userId',
|
|
||||||
access_token: 'access_token',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAuthData should throw error', async () => {
|
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ playerId: 'invalid' });
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await gpgames.validateAuthData({
|
|
||||||
id: 'userId',
|
|
||||||
access_token: 'access_token',
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe('Google Play Games Services - authData is invalid for this user.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('keycloak auth adapter', () => {
|
describe('keycloak auth adapter', () => {
|
||||||
const keycloak = require('../lib/Adapters/Auth/keycloak');
|
const keycloak = require('../lib/Adapters/Auth/keycloak');
|
||||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||||
@@ -987,433 +827,6 @@ describe('keycloak auth adapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('oauth2 auth adapter', () => {
|
|
||||||
const oauth2 = require('../lib/Adapters/Auth/oauth2');
|
|
||||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
|
||||||
|
|
||||||
it('properly loads OAuth2 adapter via the "oauth2" option', () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const loadedAuthAdapter = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
|
||||||
expect(loadedAuthAdapter.adapter).toEqual(oauth2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('properly loads OAuth2 adapter with options', () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
useridField: 'sub',
|
|
||||||
appidField: 'appId',
|
|
||||||
appIds: ['a', 'b'],
|
|
||||||
authorizationHeader: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
|
||||||
debug: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const loadedAuthAdapter = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
|
||||||
const appIds = loadedAuthAdapter.appIds;
|
|
||||||
const providerOptions = loadedAuthAdapter.providerOptions;
|
|
||||||
expect(providerOptions.tokenIntrospectionEndpointUrl).toEqual('https://example.com/introspect');
|
|
||||||
expect(providerOptions.useridField).toEqual('sub');
|
|
||||||
expect(providerOptions.appidField).toEqual('appId');
|
|
||||||
expect(appIds).toEqual(['a', 'b']);
|
|
||||||
expect(providerOptions.authorizationHeader).toEqual('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
|
|
||||||
expect(providerOptions.debug).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAppId should fail if OAuth2 tokenIntrospectionEndpointUrl is not configured properly', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
appIds: ['a', 'b'],
|
|
||||||
appidField: 'appId',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe(
|
|
||||||
'OAuth2 token introspection endpoint URL is missing from configuration!'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAppId appidField optional', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
// Should not reach here
|
|
||||||
fail(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAppId should fail without appIds', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
appidField: 'appId',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe(
|
|
||||||
'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAppId should fail empty appIds', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
appidField: 'appId',
|
|
||||||
appIds: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe(
|
|
||||||
'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAppId invalid accessToken', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
appidField: 'appId',
|
|
||||||
appIds: ['a', 'b'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
|
||||||
return Promise.resolve({});
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe('OAuth2 access token is invalid for this user.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAppId invalid accessToken appId', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
appidField: 'appId',
|
|
||||||
appIds: ['a', 'b'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
|
||||||
return Promise.resolve({ active: true });
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe(
|
|
||||||
"OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAppId valid accessToken appId', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
appidField: 'appId',
|
|
||||||
appIds: ['a', 'b'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
active: true,
|
|
||||||
appId: 'a',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
// Should not enter here
|
|
||||||
fail(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAppId valid accessToken appId array', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
appidField: 'appId',
|
|
||||||
appIds: ['a', 'b'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
active: true,
|
|
||||||
appId: ['a'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
// Should not enter here
|
|
||||||
fail(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAppId valid accessToken invalid appId', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
appidField: 'appId',
|
|
||||||
appIds: ['a', 'b'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
active: true,
|
|
||||||
appId: 'unknown',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe(
|
|
||||||
"OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAuthData should fail if OAuth2 tokenIntrospectionEndpointUrl is not configured properly', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await adapter.validateAuthData(authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe(
|
|
||||||
'OAuth2 token introspection endpoint URL is missing from configuration!'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAuthData invalid accessToken', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
useridField: 'sub',
|
|
||||||
appidField: 'appId',
|
|
||||||
appIds: ['a', 'b'],
|
|
||||||
authorizationHeader: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
|
||||||
return Promise.resolve({});
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await adapter.validateAuthData(authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.message).toBe('OAuth2 access token is invalid for this user.');
|
|
||||||
}
|
|
||||||
expect(httpsRequest.request).toHaveBeenCalledWith(
|
|
||||||
{
|
|
||||||
hostname: 'example.com',
|
|
||||||
path: '/introspect',
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Content-Length': 15,
|
|
||||||
Authorization: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'token=sometoken'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAuthData valid accessToken', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
useridField: 'sub',
|
|
||||||
appidField: 'appId',
|
|
||||||
appIds: ['a', 'b'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
active: true,
|
|
||||||
sub: 'fakeid',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await adapter.validateAuthData(authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
// Should not enter here
|
|
||||||
fail(e);
|
|
||||||
}
|
|
||||||
expect(httpsRequest.request).toHaveBeenCalledWith(
|
|
||||||
{
|
|
||||||
hostname: 'example.com',
|
|
||||||
path: '/introspect',
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Content-Length': 15,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'token=sometoken'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAuthData valid accessToken without useridField', async () => {
|
|
||||||
const options = {
|
|
||||||
oauth2Authentication: {
|
|
||||||
oauth2: true,
|
|
||||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
|
||||||
appidField: 'appId',
|
|
||||||
appIds: ['a', 'b'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const authData = {
|
|
||||||
id: 'fakeid',
|
|
||||||
access_token: 'sometoken',
|
|
||||||
};
|
|
||||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'oauth2Authentication',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
active: true,
|
|
||||||
sub: 'fakeid',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await adapter.validateAuthData(authData, providerOptions);
|
|
||||||
} catch (e) {
|
|
||||||
// Should not enter here
|
|
||||||
fail(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('apple signin auth adapter', () => {
|
describe('apple signin auth adapter', () => {
|
||||||
const apple = require('../lib/Adapters/Auth/apple');
|
const apple = require('../lib/Adapters/Auth/apple');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
@@ -1722,206 +1135,17 @@ describe('apple signin auth adapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Apple Game Center Auth adapter', () => {
|
|
||||||
const gcenter = require('../lib/Adapters/Auth/gcenter');
|
|
||||||
const fs = require('fs');
|
|
||||||
const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
|
|
||||||
const testCert2 = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
|
|
||||||
|
|
||||||
it('can load adapter', async () => {
|
|
||||||
const options = {
|
|
||||||
gcenter: {
|
|
||||||
rootCertificateUrl:
|
|
||||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'gcenter',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
await adapter.validateAppId(
|
|
||||||
appIds,
|
|
||||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
|
||||||
providerOptions
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAuthData should validate', async () => {
|
|
||||||
const options = {
|
|
||||||
gcenter: {
|
|
||||||
rootCertificateUrl:
|
|
||||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'gcenter',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
await adapter.validateAppId(
|
|
||||||
appIds,
|
|
||||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
|
||||||
providerOptions
|
|
||||||
);
|
|
||||||
// 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',
|
|
||||||
};
|
|
||||||
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
|
|
||||||
await gcenter.validateAuthData(authData);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAuthData invalid signature id', async () => {
|
|
||||||
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
|
|
||||||
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-6.cer'] = testCert2;
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'gcenter',
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
await adapter.validateAppId(
|
|
||||||
appIds,
|
|
||||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
|
||||||
providerOptions
|
|
||||||
);
|
|
||||||
const authData = {
|
|
||||||
id: 'G:1965586982',
|
|
||||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
|
|
||||||
timestamp: 1565257031287,
|
|
||||||
signature: '1234',
|
|
||||||
salt: 'DzqqrQ==',
|
|
||||||
bundleId: 'com.example.com',
|
|
||||||
};
|
|
||||||
await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
|
|
||||||
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validateAuthData invalid public key http url', async () => {
|
|
||||||
const options = {
|
|
||||||
gcenter: {
|
|
||||||
rootCertificateUrl:
|
|
||||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'gcenter',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
await adapter.validateAppId(
|
|
||||||
appIds,
|
|
||||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
|
||||||
providerOptions
|
|
||||||
);
|
|
||||||
const publicKeyUrls = [
|
|
||||||
'example.com',
|
|
||||||
'http://static.gc.apple.com/public-key/gc-prod-4.cer',
|
|
||||||
'https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg',
|
|
||||||
'https://example.com/ \\.apple.com/public_key.cer',
|
|
||||||
'https://example.com/ &.apple.com/public_key.cer',
|
|
||||||
];
|
|
||||||
await Promise.all(
|
|
||||||
publicKeyUrls.map(publicKeyUrl =>
|
|
||||||
expectAsync(
|
|
||||||
gcenter.validateAuthData({
|
|
||||||
id: 'G:1965586982',
|
|
||||||
timestamp: 1565257031287,
|
|
||||||
publicKeyUrl,
|
|
||||||
signature: '1234',
|
|
||||||
salt: 'DzqqrQ==',
|
|
||||||
bundleId: 'com.example.com',
|
|
||||||
})
|
|
||||||
).toBeRejectedWith(
|
|
||||||
new Parse.Error(
|
|
||||||
Parse.Error.SCRIPT_FAILED,
|
|
||||||
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not validate Symantec Cert', async () => {
|
|
||||||
const options = {
|
|
||||||
gcenter: {
|
|
||||||
rootCertificateUrl:
|
|
||||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'gcenter',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
await adapter.validateAppId(
|
|
||||||
appIds,
|
|
||||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
|
||||||
providerOptions
|
|
||||||
);
|
|
||||||
expect(() =>
|
|
||||||
gcenter.verifyPublicKeyIssuer(
|
|
||||||
testCert,
|
|
||||||
'https://static.gc.apple.com/public-key/gc-prod-4.cer'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adapter should load default cert', async () => {
|
|
||||||
const options = {
|
|
||||||
gcenter: {},
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'gcenter',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
await adapter.validateAppId(
|
|
||||||
appIds,
|
|
||||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
|
||||||
providerOptions
|
|
||||||
);
|
|
||||||
const previous = new Date();
|
|
||||||
await adapter.validateAppId(
|
|
||||||
appIds,
|
|
||||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
|
||||||
providerOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const duration = new Date().getTime() - previous.getTime();
|
|
||||||
expect(duration <= 1).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adapter should throw', async () => {
|
|
||||||
const options = {
|
|
||||||
gcenter: {
|
|
||||||
rootCertificateUrl: 'https://example.com',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
||||||
'gcenter',
|
|
||||||
options
|
|
||||||
);
|
|
||||||
await expectAsync(
|
|
||||||
adapter.validateAppId(
|
|
||||||
appIds,
|
|
||||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
|
||||||
providerOptions
|
|
||||||
)
|
|
||||||
).toBeRejectedWith(
|
|
||||||
new Parse.Error(
|
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
|
||||||
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('phant auth adapter', () => {
|
describe('phant auth adapter', () => {
|
||||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||||
|
|
||||||
it('validateAuthData should throw for invalid auth', async () => {
|
it('validateAuthData should throw for invalid auth', async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
auth: {
|
||||||
|
phantauth: {
|
||||||
|
enableInsecureAuth: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
const authData = {
|
const authData = {
|
||||||
id: 'fakeid',
|
id: 'fakeid',
|
||||||
access_token: 'sometoken',
|
access_token: 'sometoken',
|
||||||
@@ -1938,34 +1162,6 @@ describe('phant auth adapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('microsoft graph auth adapter', () => {
|
|
||||||
const microsoft = require('../lib/Adapters/Auth/microsoft');
|
|
||||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
|
||||||
|
|
||||||
it('should use access_token for validation is passed and responds with id and mail', async () => {
|
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ id: 'userId', mail: 'userMail' });
|
|
||||||
});
|
|
||||||
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(Parse.Error.OBJECT_NOT_FOUND);
|
|
||||||
expect(err.message).toBe('Microsoft Graph auth is invalid for this user.');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('facebook limited auth adapter', () => {
|
describe('facebook limited auth adapter', () => {
|
||||||
const facebook = require('../lib/Adapters/Auth/facebook');
|
const facebook = require('../lib/Adapters/Auth/facebook');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
|||||||
@@ -355,16 +355,16 @@ describe('Auth Adapter features', () => {
|
|||||||
const authData = user.get('authData').modernAdapter3;
|
const authData = user.get('authData').modernAdapter3;
|
||||||
expect(authData).toEqual({ foo: 'bar' });
|
expect(authData).toEqual({ foo: 'bar' });
|
||||||
for (const call of afterSpy.calls.all()) {
|
for (const call of afterSpy.calls.all()) {
|
||||||
const args = call.args[0];
|
const args = call.args[2];
|
||||||
if (args.user) {
|
if (args.user) {
|
||||||
user._objCount = args.user._objCount;
|
user._objCount = args.user._objCount;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
expect(afterSpy).toHaveBeenCalledWith(
|
expect(afterSpy).toHaveBeenCalledWith(
|
||||||
{ ip: '127.0.0.1', user, master: false },
|
|
||||||
{ id: 'modernAdapter3Data' },
|
{ id: 'modernAdapter3Data' },
|
||||||
undefined
|
undefined,
|
||||||
|
{ ip: '127.0.0.1', user, master: false },
|
||||||
);
|
);
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ describe('Security Check Groups', () => {
|
|||||||
config.masterKey = 'aMoreSecur3Passwor7!';
|
config.masterKey = 'aMoreSecur3Passwor7!';
|
||||||
config.security.enableCheckLog = false;
|
config.security.enableCheckLog = false;
|
||||||
config.allowClientClassCreation = false;
|
config.allowClientClassCreation = false;
|
||||||
|
config.enableInsecureAuthAdapters = false;
|
||||||
await reconfigureServer(config);
|
await reconfigureServer(config);
|
||||||
|
|
||||||
const group = new CheckGroupServerConfig();
|
const group = new CheckGroupServerConfig();
|
||||||
@@ -39,6 +40,7 @@ describe('Security Check Groups', () => {
|
|||||||
expect(group.checks()[0].checkState()).toBe(CheckState.success);
|
expect(group.checks()[0].checkState()).toBe(CheckState.success);
|
||||||
expect(group.checks()[1].checkState()).toBe(CheckState.success);
|
expect(group.checks()[1].checkState()).toBe(CheckState.success);
|
||||||
expect(group.checks()[2].checkState()).toBe(CheckState.success);
|
expect(group.checks()[2].checkState()).toBe(CheckState.success);
|
||||||
|
expect(group.checks()[4].checkState()).toBe(CheckState.success);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks fail correctly', async () => {
|
it('checks fail correctly', async () => {
|
||||||
@@ -52,6 +54,7 @@ describe('Security Check Groups', () => {
|
|||||||
expect(group.checks()[0].checkState()).toBe(CheckState.fail);
|
expect(group.checks()[0].checkState()).toBe(CheckState.fail);
|
||||||
expect(group.checks()[1].checkState()).toBe(CheckState.fail);
|
expect(group.checks()[1].checkState()).toBe(CheckState.fail);
|
||||||
expect(group.checks()[2].checkState()).toBe(CheckState.fail);
|
expect(group.checks()[2].checkState()).toBe(CheckState.fail);
|
||||||
|
expect(group.checks()[4].checkState()).toBe(CheckState.fail);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
const twitter = require('../lib/Adapters/Auth/twitter');
|
|
||||||
|
|
||||||
describe('Twitter Auth', () => {
|
|
||||||
it('should use the proper configuration', () => {
|
|
||||||
// Multiple options, consumer_key found
|
|
||||||
expect(
|
|
||||||
twitter.handleMultipleConfigurations(
|
|
||||||
{
|
|
||||||
consumer_key: 'hello',
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
consumer_key: 'hello',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
consumer_key: 'world',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
).consumer_key
|
|
||||||
).toEqual('hello');
|
|
||||||
|
|
||||||
// Multiple options, consumer_key not found
|
|
||||||
expect(function () {
|
|
||||||
twitter.handleMultipleConfigurations(
|
|
||||||
{
|
|
||||||
consumer_key: 'some',
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
consumer_key: 'hello',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
consumer_key: 'world',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}).toThrow();
|
|
||||||
|
|
||||||
// Multiple options, consumer_key not found
|
|
||||||
expect(function () {
|
|
||||||
twitter.handleMultipleConfigurations(
|
|
||||||
{
|
|
||||||
auth_token: 'token',
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
consumer_key: 'hello',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
consumer_key: 'world',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}).toThrow();
|
|
||||||
|
|
||||||
// Single configuration and consumer_key set
|
|
||||||
expect(
|
|
||||||
twitter.handleMultipleConfigurations(
|
|
||||||
{
|
|
||||||
consumer_key: 'hello',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
consumer_key: 'hello',
|
|
||||||
}
|
|
||||||
).consumer_key
|
|
||||||
).toEqual('hello');
|
|
||||||
|
|
||||||
// General case, only 1 config, no consumer_key set
|
|
||||||
expect(
|
|
||||||
twitter.handleMultipleConfigurations(
|
|
||||||
{
|
|
||||||
auth_token: 'token',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
consumer_key: 'hello',
|
|
||||||
}
|
|
||||||
).consumer_key
|
|
||||||
).toEqual('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should fail with missing options', done => {
|
|
||||||
try {
|
|
||||||
twitter.validateAuthData(
|
|
||||||
{
|
|
||||||
consumer_key: 'key',
|
|
||||||
consumer_secret: 'secret',
|
|
||||||
auth_token: 'token',
|
|
||||||
auth_token_secret: 'secret',
|
|
||||||
},
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
jequal(error.message, 'Twitter auth configuration missing');
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -9,6 +9,7 @@ module.exports = [
|
|||||||
globals: {
|
globals: {
|
||||||
...globals.node,
|
...globals.node,
|
||||||
...globals.jasmine,
|
...globals.jasmine,
|
||||||
|
mockFetch: "readonly",
|
||||||
Parse: "readonly",
|
Parse: "readonly",
|
||||||
reconfigureServer: "readonly",
|
reconfigureServer: "readonly",
|
||||||
createTestUser: "readonly",
|
createTestUser: "readonly",
|
||||||
|
|||||||
@@ -414,6 +414,25 @@ function mockShortLivedAuth() {
|
|||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockFetch(mockResponses) {
|
||||||
|
global.fetch = jasmine.createSpy('fetch').and.callFake((url, options = { }) => {
|
||||||
|
options.method ||= 'GET';
|
||||||
|
const mockResponse = mockResponses.find(
|
||||||
|
(mock) => mock.url === url && mock.method === options.method
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mockResponse) {
|
||||||
|
return Promise.resolve(mockResponse.response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
statusText: 'Unknown URL or method',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// This is polluting, but, it makes it way easier to directly port old tests.
|
// This is polluting, but, it makes it way easier to directly port old tests.
|
||||||
global.Parse = Parse;
|
global.Parse = Parse;
|
||||||
global.TestObject = TestObject;
|
global.TestObject = TestObject;
|
||||||
@@ -429,6 +448,7 @@ global.arrayContains = arrayContains;
|
|||||||
global.jequal = jequal;
|
global.jequal = jequal;
|
||||||
global.range = range;
|
global.range = range;
|
||||||
global.reconfigureServer = reconfigureServer;
|
global.reconfigureServer = reconfigureServer;
|
||||||
|
global.mockFetch = mockFetch;
|
||||||
global.defaultConfiguration = defaultConfiguration;
|
global.defaultConfiguration = defaultConfiguration;
|
||||||
global.mockCustomAuthenticator = mockCustomAuthenticator;
|
global.mockCustomAuthenticator = mockCustomAuthenticator;
|
||||||
global.mockFacebookAuthenticator = mockFacebookAuthenticator;
|
global.mockFacebookAuthenticator = mockFacebookAuthenticator;
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIGsDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBi
|
||||||
|
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||||
|
d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
|
||||||
|
RzQwHhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJV
|
||||||
|
UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRy
|
||||||
|
dXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIIC
|
||||||
|
IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1
|
||||||
|
M4zrPYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZ
|
||||||
|
wZHMgQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI
|
||||||
|
8IrgnQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGi
|
||||||
|
TUyCEUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLm
|
||||||
|
ysL0p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3S
|
||||||
|
vUQakhCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tv
|
||||||
|
k2E0XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+
|
||||||
|
960IHnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3s
|
||||||
|
MJN2FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FK
|
||||||
|
PkBHX8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1H
|
||||||
|
s/q27IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAw
|
||||||
|
HQYDVR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LS
|
||||||
|
cV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEF
|
||||||
|
BQcDAzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
|
||||||
|
Z2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
|
||||||
|
Y29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYy
|
||||||
|
aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5j
|
||||||
|
cmwwHAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQAD
|
||||||
|
ggIBADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L
|
||||||
|
/Z6jfCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHV
|
||||||
|
UHmImoqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rd
|
||||||
|
KOtfJqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK
|
||||||
|
6Wrxoj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43N
|
||||||
|
b3Y3LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4Z
|
||||||
|
XDlx4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvm
|
||||||
|
oLr9Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8
|
||||||
|
y4+ICw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMM
|
||||||
|
B0ug0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+F
|
||||||
|
SCH5Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhO
|
||||||
|
-----END CERTIFICATE-----
|
||||||
BIN
spec/support/cert/gc-prod-4.cer
Normal file
BIN
spec/support/cert/gc-prod-4.cer
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"spec_dir": "spec",
|
"spec_dir": "spec",
|
||||||
"spec_files": ["*spec.js"],
|
"spec_files": ["**/*.[sS]pec.js"],
|
||||||
"helpers": ["helper.js"],
|
"helpers": ["helper.js"],
|
||||||
"random": true
|
"random": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ export class AuthAdapter {
|
|||||||
* Legacy usage, if provided it will be triggered when authData related to this provider is touched (signup/update/login)
|
* Legacy usage, if provided it will be triggered when authData related to this provider is touched (signup/update/login)
|
||||||
* otherwise you should implement validateSetup, validateLogin and validateUpdate
|
* otherwise you should implement validateSetup, validateLogin and validateUpdate
|
||||||
* @param {Object} authData The client provided authData
|
* @param {Object} authData The client provided authData
|
||||||
* @param {Parse.Cloud.TriggerRequest} request
|
|
||||||
* @param {Object} options additional adapter options
|
* @param {Object} options additional adapter options
|
||||||
|
* @param {Parse.Cloud.TriggerRequest} request
|
||||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||||
*/
|
*/
|
||||||
validateAuthData(authData, request, options) {
|
validateAuthData(authData, options, request) {
|
||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +52,11 @@ export class AuthAdapter {
|
|||||||
* Triggered when user provide for the first time this auth provider
|
* Triggered when user provide for the first time this auth provider
|
||||||
* could be a register or the user adding a new auth service
|
* could be a register or the user adding a new auth service
|
||||||
* @param {Object} authData The client provided authData
|
* @param {Object} authData The client provided authData
|
||||||
* @param {Parse.Cloud.TriggerRequest} request
|
|
||||||
* @param {Object} options additional adapter options
|
* @param {Object} options additional adapter options
|
||||||
|
* @param {Parse.Cloud.TriggerRequest} request
|
||||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||||
*/
|
*/
|
||||||
validateSetUp(authData, req, options) {
|
validateSetUp(authData, options, req) {
|
||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,11 +64,11 @@ export class AuthAdapter {
|
|||||||
* Triggered when user provide authData related to this provider
|
* Triggered when user provide authData related to this provider
|
||||||
* The user is not logged in and has already set this provider before
|
* The user is not logged in and has already set this provider before
|
||||||
* @param {Object} authData The client provided authData
|
* @param {Object} authData The client provided authData
|
||||||
* @param {Parse.Cloud.TriggerRequest} request
|
|
||||||
* @param {Object} options additional adapter options
|
* @param {Object} options additional adapter options
|
||||||
|
* @param {Parse.Cloud.TriggerRequest} request
|
||||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||||
*/
|
*/
|
||||||
validateLogin(authData, req, options) {
|
validateLogin(authData, options, req) {
|
||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,10 +80,18 @@ export class AuthAdapter {
|
|||||||
* @param {Parse.Cloud.TriggerRequest} request
|
* @param {Parse.Cloud.TriggerRequest} request
|
||||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||||
*/
|
*/
|
||||||
validateUpdate(authData, req, options) {
|
validateUpdate(authData, options, req) {
|
||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when user is looked up by authData with this provider. Override the `id` field if needed.
|
||||||
|
* @param {Object} authData The client provided authData
|
||||||
|
*/
|
||||||
|
beforeFind(authData) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggered in pre authentication process if needed (like webauthn, SMS OTP)
|
* Triggered in pre authentication process if needed (like webauthn, SMS OTP)
|
||||||
* @param {Object} challengeData Data provided by the client
|
* @param {Object} challengeData Data provided by the client
|
||||||
@@ -100,9 +108,10 @@ export class AuthAdapter {
|
|||||||
* Triggered when auth data is fetched
|
* Triggered when auth data is fetched
|
||||||
* @param {Object} authData authData
|
* @param {Object} authData authData
|
||||||
* @param {Object} options additional adapter options
|
* @param {Object} options additional adapter options
|
||||||
|
* @param {Parse.Cloud.TriggerRequest} request
|
||||||
* @returns {Promise<Object>} Any overrides required to authData
|
* @returns {Promise<Object>} Any overrides required to authData
|
||||||
*/
|
*/
|
||||||
afterFind(authData, options) {
|
afterFind(authData, options, request) {
|
||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
112
src/Adapters/Auth/BaseCodeAuthAdapter.js
Normal file
112
src/Adapters/Auth/BaseCodeAuthAdapter.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// abstract class for auth code adapters
|
||||||
|
import AuthAdapter from './AuthAdapter';
|
||||||
|
export default class BaseAuthCodeAdapter extends AuthAdapter {
|
||||||
|
constructor(adapterName) {
|
||||||
|
super();
|
||||||
|
this.adapterName = adapterName;
|
||||||
|
}
|
||||||
|
validateOptions(options) {
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
throw new Error(`${this.adapterName} options are required.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enableInsecureAuth = options.enableInsecureAuth;
|
||||||
|
if (this.enableInsecureAuth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clientId = options.clientId;
|
||||||
|
this.clientSecret = options.clientSecret;
|
||||||
|
|
||||||
|
if (!this.clientId) {
|
||||||
|
throw new Error(`${this.adapterName} clientId is required.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.clientSecret) {
|
||||||
|
throw new Error(`${this.adapterName} clientSecret is required.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeFind(authData) {
|
||||||
|
if (this.enableInsecureAuth && !authData?.code) {
|
||||||
|
if (!authData?.access_token) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.getUserFromAccessToken(authData.access_token, authData);
|
||||||
|
|
||||||
|
if (user.id !== authData.id) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authData?.code) {
|
||||||
|
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `${this.adapterName} code is required.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const access_token = await this.getAccessTokenFromCode(authData);
|
||||||
|
const user = await this.getUserFromAccessToken(access_token, authData);
|
||||||
|
|
||||||
|
if (authData.id && user.id !== authData.id) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
authData.access_token = access_token;
|
||||||
|
authData.id = user.id;
|
||||||
|
|
||||||
|
delete authData.code;
|
||||||
|
delete authData.redirect_uri;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserFromAccessToken() {
|
||||||
|
// abstract method
|
||||||
|
throw new Error('getUserFromAccessToken is not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessTokenFromCode() {
|
||||||
|
// abstract method
|
||||||
|
throw new Error('getAccessTokenFromCode is not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
validateLogin(authData) {
|
||||||
|
// User validation is already done in beforeFind
|
||||||
|
return {
|
||||||
|
id: authData.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateSetUp(authData) {
|
||||||
|
// User validation is already done in beforeFind
|
||||||
|
return {
|
||||||
|
id: authData.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterFind(authData) {
|
||||||
|
return {
|
||||||
|
id: authData.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateUpdate(authData) {
|
||||||
|
// User validation is already done in beforeFind
|
||||||
|
return {
|
||||||
|
id: authData.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
parseResponseData(data) {
|
||||||
|
const startPos = data.indexOf('(');
|
||||||
|
const endPos = data.indexOf(')');
|
||||||
|
if (startPos === -1 || endPos === -1) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
|
||||||
|
}
|
||||||
|
const jsonData = data.substring(startPos + 1, endPos);
|
||||||
|
return JSON.parse(jsonData);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Parse Server authentication adapter for Apple.
|
||||||
|
*
|
||||||
|
* @class AppleAdapter
|
||||||
|
* @param {Object} options - Configuration options for the adapter.
|
||||||
|
* @param {string} options.clientId - Your Apple App ID.
|
||||||
|
*
|
||||||
|
* @param {Object} authData - The authentication data provided by the client.
|
||||||
|
* @param {string} authData.id - The user ID obtained from Apple.
|
||||||
|
* @param {string} authData.token - The token obtained from Apple.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Apple authentication, use the following structure:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "apple": {
|
||||||
|
* "clientId": "12345"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Expected `authData` from the Client
|
||||||
|
* The adapter expects the client to provide the following `authData` payload:
|
||||||
|
* - `authData.id` (**string**, required): The user ID obtained from Apple.
|
||||||
|
* - `authData.token` (**string**, required): The token obtained from Apple.
|
||||||
|
*
|
||||||
|
* Parse Server stores the required authentication data in the database.
|
||||||
|
*
|
||||||
|
* ### Example AuthData from Apple
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "apple": {
|
||||||
|
* "id": "1234567",
|
||||||
|
* "token": "xxxxx.yyyyy.zzzzz"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @see {@link https://developer.apple.com/documentation/signinwithapplerestapi Sign in with Apple REST API Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
// Apple SignIn Auth
|
// Apple SignIn Auth
|
||||||
// https://developer.apple.com/documentation/signinwithapplerestapi
|
// https://developer.apple.com/documentation/signinwithapplerestapi
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Parse Server authentication adapter for Facebook.
|
||||||
|
*
|
||||||
|
* @class FacebookAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.appSecret - Your Facebook App Secret. Required for secure authentication.
|
||||||
|
* @param {string[]} options.appIds - An array of Facebook App IDs. Required for validating the app.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Facebook authentication, use the following structure:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "facebook": {
|
||||||
|
* "appSecret": "your-app-secret",
|
||||||
|
* "appIds": ["your-app-id"]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter supports the following authentication methods:
|
||||||
|
* - **Standard Login**: Requires `id` and `access_token`.
|
||||||
|
* - **Limited Login**: Requires `id` and `token`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Standard Login Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "facebook": {
|
||||||
|
* "id": "1234567",
|
||||||
|
* "access_token": "abc123def456ghi789"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Limited Login Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "facebook": {
|
||||||
|
* "id": "1234567",
|
||||||
|
* "token": "xxxxx.yyyyy.zzzzz"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - **Standard Login**: Use `id` and `access_token` for full functionality.
|
||||||
|
* - **Limited Login**: Use `id` and `token` (JWT) when tracking is opted out (e.g., via Apple's App Tracking Transparency).
|
||||||
|
* - Supported Parse Server versions:
|
||||||
|
* - `>= 6.5.6 < 7`
|
||||||
|
* - `>= 7.0.1`
|
||||||
|
*
|
||||||
|
* Secure authentication is recommended to ensure proper data protection and compliance with Facebook's guidelines.
|
||||||
|
*
|
||||||
|
* @see {@link https://developers.facebook.com/docs/facebook-login/limited-login/ Facebook Limited Login}
|
||||||
|
* @see {@link https://developers.facebook.com/docs/facebook-login/facebook-login-for-business/ Facebook Login for Business}
|
||||||
|
*/
|
||||||
|
|
||||||
// Helper functions for accessing the Facebook Graph API.
|
// Helper functions for accessing the Facebook Graph API.
|
||||||
const Parse = require('parse/node').Parse;
|
const Parse = require('parse/node').Parse;
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|||||||
@@ -1,195 +1,239 @@
|
|||||||
/* Apple Game Center Auth
|
/**
|
||||||
https://developer.apple.com/documentation/gamekit/gklocalplayer/1515407-generateidentityverificationsign#discussion
|
* Parse Server authentication adapter for Apple Game Center.
|
||||||
|
*
|
||||||
|
* @class AppleGameCenterAdapter
|
||||||
|
* @param {Object} options - Configuration options for the adapter.
|
||||||
|
* @param {string} options.bundleId - Your Apple Game Center bundle ID. Required for secure authentication.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
*
|
||||||
|
* @param {Object} authData - The authentication data provided by the client.
|
||||||
|
* @param {string} authData.id - The user ID obtained from Apple Game Center.
|
||||||
|
* @param {string} authData.publicKeyUrl - The public key URL obtained from Apple Game Center.
|
||||||
|
* @param {string} authData.timestamp - The timestamp obtained from Apple Game Center.
|
||||||
|
* @param {string} authData.signature - The signature obtained from Apple Game Center.
|
||||||
|
* @param {string} authData.salt - The salt obtained from Apple Game Center.
|
||||||
|
* @param {string} [authData.bundleId] - **[DEPRECATED]** The bundle ID obtained from Apple Game Center (required for insecure authentication).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* The following `authData` fields are required:
|
||||||
|
* `id`, `publicKeyUrl`, `timestamp`, `signature`, and `salt`. These fields are validated against the configured `bundleId` for additional security.
|
||||||
|
*
|
||||||
|
* To configure Parse Server for Apple Game Center authentication, use the following structure:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "gcenter": {
|
||||||
|
* "bundleId": "com.valid.app"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Insecure Authentication (Not Recommended)
|
||||||
|
* The following `authData` fields are required for insecure authentication:
|
||||||
|
* `id`, `publicKeyUrl`, `timestamp`, `signature`, `salt`, and `bundleId` (**[DEPRECATED]**). This flow is insecure and poses potential security risks.
|
||||||
|
*
|
||||||
|
* To configure Parse Server for insecure authentication, use the following structure:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "gcenter": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Deprecation Notice
|
||||||
|
* The `enableInsecureAuth` option and `authData.bundleId` parameter are deprecated and may be removed in future releases. Use secure authentication with the `bundleId` configured in the `options` object instead.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @example <caption>Secure Authentication Example</caption>
|
||||||
|
* // Example authData for secure authentication:
|
||||||
|
* const authData = {
|
||||||
|
* gcenter: {
|
||||||
|
* id: "1234567",
|
||||||
|
* publicKeyUrl: "https://valid.apple.com/public/timeout.cer",
|
||||||
|
* timestamp: 1460981421303,
|
||||||
|
* salt: "saltST==",
|
||||||
|
* signature: "PoDwf39DCN464B49jJCU0d9Y0J"
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @example <caption>Insecure Authentication Example (Not Recommended)</caption>
|
||||||
|
* // Example authData for insecure authentication:
|
||||||
|
* const authData = {
|
||||||
|
* gcenter: {
|
||||||
|
* id: "1234567",
|
||||||
|
* publicKeyUrl: "https://valid.apple.com/public/timeout.cer",
|
||||||
|
* timestamp: 1460981421303,
|
||||||
|
* salt: "saltST==",
|
||||||
|
* signature: "PoDwf39DCN464B49jJCU0d9Y0J",
|
||||||
|
* bundleId: "com.valid.app" // Deprecated.
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @see {@link https://developer.apple.com/documentation/gamekit/gklocalplayer/3516283-fetchitems Apple Game Center Documentation}
|
||||||
|
*/
|
||||||
|
/* global BigInt */
|
||||||
|
|
||||||
const authData = {
|
import crypto from 'crypto';
|
||||||
publicKeyUrl: 'https://valid.apple.com/public/timeout.cer',
|
import { asn1, pki } from 'node-forge';
|
||||||
timestamp: 1460981421303,
|
import AuthAdapter from './AuthAdapter';
|
||||||
signature: 'PoDwf39DCN464B49jJCU0d9Y0J',
|
class GameCenterAuth extends AuthAdapter {
|
||||||
salt: 'saltST==',
|
constructor() {
|
||||||
bundleId: 'com.valid.app'
|
super();
|
||||||
id: 'playerId',
|
this.ca = { cert: null, url: null };
|
||||||
};
|
this.cache = {};
|
||||||
*/
|
this.bundleId = '';
|
||||||
|
}
|
||||||
|
|
||||||
const { Parse } = require('parse/node');
|
validateOptions(options) {
|
||||||
const crypto = require('crypto');
|
if (!options) {
|
||||||
const https = require('https');
|
throw new Error('Game center auth options are required.');
|
||||||
const { pki } = require('node-forge');
|
}
|
||||||
const ca = { cert: null, url: null };
|
|
||||||
const cache = {}; // (publicKey -> cert) cache
|
|
||||||
|
|
||||||
function verifyPublicKeyUrl(publicKeyUrl) {
|
if (!this.loadingPromise) {
|
||||||
try {
|
this.loadingPromise = this.loadCertificate(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enableInsecureAuth = options.enableInsecureAuth;
|
||||||
|
this.bundleId = options.bundleId;
|
||||||
|
|
||||||
|
if (!this.enableInsecureAuth && !this.bundleId) {
|
||||||
|
throw new Error('bundleId is required for secure auth.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCertificate(options) {
|
||||||
|
const rootCertificateUrl =
|
||||||
|
options.rootCertificateUrl ||
|
||||||
|
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
|
||||||
|
|
||||||
|
if (this.ca.url === rootCertificateUrl) {
|
||||||
|
return rootCertificateUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { certificate, headers } = await this.fetchCertificate(rootCertificateUrl);
|
||||||
|
|
||||||
|
if (
|
||||||
|
headers.get('content-type') !== 'application/x-pem-file' ||
|
||||||
|
!headers.get('content-length') ||
|
||||||
|
parseInt(headers.get('content-length'), 10) > 10000
|
||||||
|
) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid rootCertificateURL.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ca.cert = pki.certificateFromPem(certificate);
|
||||||
|
this.ca.url = rootCertificateUrl;
|
||||||
|
|
||||||
|
return rootCertificateUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyPublicKeyUrl(publicKeyUrl) {
|
||||||
const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
|
const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
|
||||||
return regex.test(publicKeyUrl);
|
return regex.test(publicKeyUrl);
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function convertX509CertToPEM(X509Cert) {
|
async fetchCertificate(url) {
|
||||||
const pemPreFix = '-----BEGIN CERTIFICATE-----\n';
|
const response = await fetch(url);
|
||||||
const pemPostFix = '-----END CERTIFICATE-----';
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch certificate: ${url}`);
|
||||||
const base64 = X509Cert;
|
|
||||||
const certBody = base64.match(new RegExp('.{0,64}', 'g')).join('\n');
|
|
||||||
|
|
||||||
return pemPreFix + certBody + pemPostFix;
|
|
||||||
}
|
|
||||||
|
|
||||||
async 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];
|
|
||||||
}
|
|
||||||
const url = new URL(publicKeyUrl);
|
|
||||||
const headOptions = {
|
|
||||||
hostname: url.hostname,
|
|
||||||
path: url.pathname,
|
|
||||||
method: 'HEAD',
|
|
||||||
};
|
|
||||||
const cert_headers = await new Promise((resolve, reject) =>
|
|
||||||
https.get(headOptions, res => resolve(res.headers)).on('error', reject)
|
|
||||||
);
|
|
||||||
const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
|
|
||||||
if (
|
|
||||||
!validContentTypes.includes(cert_headers['content-type']) ||
|
|
||||||
cert_headers['content-length'] == null ||
|
|
||||||
cert_headers['content-length'] > 10000
|
|
||||||
) {
|
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
|
||||||
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { certificate, headers } = await getCertificate(publicKeyUrl);
|
|
||||||
if (headers['cache-control']) {
|
|
||||||
const expire = headers['cache-control'].match(/max-age=([0-9]+)/);
|
|
||||||
if (expire) {
|
|
||||||
cache[publicKeyUrl] = certificate;
|
|
||||||
// we'll expire the cache entry later, as per max-age
|
|
||||||
setTimeout(() => {
|
|
||||||
delete cache[publicKeyUrl];
|
|
||||||
}, parseInt(expire[1], 10) * 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
const isPem = contentType?.includes('application/x-pem-file');
|
||||||
|
|
||||||
|
if (isPem) {
|
||||||
|
const certificate = await response.text();
|
||||||
|
return { certificate, headers: response.headers };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.arrayBuffer();
|
||||||
|
const binaryData = Buffer.from(data);
|
||||||
|
|
||||||
|
const asn1Cert = asn1.fromDer(binaryData.toString('binary'));
|
||||||
|
const forgeCert = pki.certificateFromAsn1(asn1Cert);
|
||||||
|
const certificate = pki.certificateToPem(forgeCert);
|
||||||
|
|
||||||
|
return { certificate, headers: response.headers };
|
||||||
}
|
}
|
||||||
return verifyPublicKeyIssuer(certificate, publicKeyUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCertificate(url, buffer) {
|
async getAppleCertificate(publicKeyUrl) {
|
||||||
return new Promise((resolve, reject) => {
|
if (!this.verifyPublicKeyUrl(publicKeyUrl)) {
|
||||||
https
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `Invalid publicKeyUrl: ${publicKeyUrl}`);
|
||||||
.get(url, res => {
|
}
|
||||||
const data = [];
|
|
||||||
res.on('data', chunk => {
|
|
||||||
data.push(chunk);
|
|
||||||
});
|
|
||||||
res.on('end', () => {
|
|
||||||
if (buffer) {
|
|
||||||
resolve({ certificate: Buffer.concat(data), headers: res.headers });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let cert = '';
|
|
||||||
for (const chunk of data) {
|
|
||||||
cert += chunk.toString('base64');
|
|
||||||
}
|
|
||||||
const certificate = convertX509CertToPEM(cert);
|
|
||||||
resolve({ certificate, headers: res.headers });
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.on('error', reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertTimestampToBigEndian(timestamp) {
|
if (this.cache[publicKeyUrl]) {
|
||||||
const buffer = Buffer.alloc(8);
|
return this.cache[publicKeyUrl];
|
||||||
|
}
|
||||||
|
|
||||||
const high = ~~(timestamp / 0xffffffff);
|
const { certificate, headers } = await this.fetchCertificate(publicKeyUrl);
|
||||||
const low = timestamp % (0xffffffff + 0x1);
|
const cacheControl = headers.get('cache-control');
|
||||||
|
const expire = cacheControl?.match(/max-age=([0-9]+)/);
|
||||||
|
|
||||||
buffer.writeUInt32BE(parseInt(high, 10), 0);
|
this.verifyPublicKeyIssuer(certificate, publicKeyUrl);
|
||||||
buffer.writeUInt32BE(parseInt(low, 10), 4);
|
|
||||||
|
|
||||||
return buffer;
|
if (expire) {
|
||||||
}
|
this.cache[publicKeyUrl] = certificate;
|
||||||
|
setTimeout(() => delete this.cache[publicKeyUrl], parseInt(expire[1], 10) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
function verifySignature(publicKey, authData) {
|
return certificate;
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function verifyPublicKeyIssuer(cert, publicKeyUrl) {
|
verifyPublicKeyIssuer(cert, publicKeyUrl) {
|
||||||
const publicKeyCert = pki.certificateFromPem(cert);
|
const publicKeyCert = pki.certificateFromPem(cert);
|
||||||
if (!ca.cert) {
|
|
||||||
throw new Parse.Error(
|
if (!this.ca.cert) {
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
|
||||||
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (!ca.cert.verify(publicKeyCert)) {
|
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
|
'Root certificate is invalid or missing.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
throw new Parse.Error(
|
if (!this.ca.cert.verify(publicKeyCert)) {
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `Invalid publicKeyUrl: ${publicKeyUrl}`);
|
||||||
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
|
verifySignature(publicKey, authData) {
|
||||||
|
const bundleId = this.bundleId || (this.enableInsecureAuth && authData.bundleId);
|
||||||
|
|
||||||
|
const verifier = crypto.createVerify('sha256');
|
||||||
|
verifier.update(Buffer.from(authData.id, 'utf8'));
|
||||||
|
verifier.update(Buffer.from(bundleId, 'utf8'));
|
||||||
|
verifier.update(this.convertTimestampToBigEndian(authData.timestamp));
|
||||||
|
verifier.update(Buffer.from(authData.salt, 'base64'));
|
||||||
|
|
||||||
|
if (!verifier.verify(publicKey, authData.signature, 'base64')) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid signature.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAuthData(authData) {
|
||||||
|
|
||||||
|
const requiredKeys = ['id', 'publicKeyUrl', 'timestamp', 'signature', 'salt'];
|
||||||
|
if (this.enableInsecureAuth) {
|
||||||
|
requiredKeys.push('bundleId');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of requiredKeys) {
|
||||||
|
if (!authData[key]) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `AuthData ${key} is missing.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadingPromise;
|
||||||
|
|
||||||
|
const publicKey = await this.getAppleCertificate(authData.publicKeyUrl);
|
||||||
|
this.verifySignature(publicKey, authData);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertTimestampToBigEndian(timestamp) {
|
||||||
|
const buffer = Buffer.alloc(8);
|
||||||
|
buffer.writeBigUInt64BE(BigInt(timestamp));
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
return cert;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills if this user id is valid.
|
export default new GameCenterAuth();
|
||||||
async function validateAuthData(authData) {
|
|
||||||
if (!authData.id) {
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, '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.
|
|
||||||
async function validateAppId(appIds, authData, options = {}) {
|
|
||||||
if (!options.rootCertificateUrl) {
|
|
||||||
options.rootCertificateUrl =
|
|
||||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
|
|
||||||
}
|
|
||||||
if (ca.url === options.rootCertificateUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true);
|
|
||||||
if (
|
|
||||||
headers['content-type'] !== 'application/x-pem-file' ||
|
|
||||||
headers['content-length'] == null ||
|
|
||||||
headers['content-length'] > 10000
|
|
||||||
) {
|
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
|
||||||
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ca.cert = pki.certificateFromPem(certificate);
|
|
||||||
ca.url = options.rootCertificateUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAppId,
|
|
||||||
validateAuthData,
|
|
||||||
cache,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,35 +1,127 @@
|
|||||||
// Helper functions for accessing the github API.
|
/**
|
||||||
var Parse = require('parse/node').Parse;
|
* Parse Server authentication adapter for GitHub.
|
||||||
const httpsRequest = require('./httpsRequest');
|
* @class GitHubAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.clientId - The GitHub App Client ID. Required for secure authentication.
|
||||||
|
* @param {string} options.clientSecret - The GitHub App Client Secret. Required for secure authentication.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
*
|
||||||
|
* @param {Object} authData - The authentication data provided by the client.
|
||||||
|
* @param {string} authData.code - The authorization code from GitHub. Required for secure authentication.
|
||||||
|
* @param {string} [authData.id] - **[DEPRECATED]** The GitHub user ID (required for insecure authentication).
|
||||||
|
* @param {string} [authData.access_token] - **[DEPRECATED]** The GitHub access token (required for insecure authentication).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* * To configure Parse Server for GitHub authentication, use the following structure:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "github": {
|
||||||
|
* "clientId": "12345",
|
||||||
|
* "clientSecret": "abcde"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The GitHub adapter exchanges the `authData.code` provided by the client for an access token using GitHub's OAuth API. The following `authData` field is required:
|
||||||
|
* - `code`
|
||||||
|
*
|
||||||
|
* ## Insecure Authentication (Not Recommended)
|
||||||
|
* Insecure authentication uses the `authData.id` and `authData.access_token` provided by the client. This flow is insecure, deprecated, and poses potential security risks. The following `authData` fields are required:
|
||||||
|
* - `id` (**[DEPRECATED]**): The GitHub user ID.
|
||||||
|
* - `access_token` (**[DEPRECATED]**): The GitHub access token.
|
||||||
|
* To configure Parse Server for insecure authentication, use the following structure:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "github": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Deprecation Notice
|
||||||
|
* The `enableInsecureAuth` option and insecure `authData` fields (`id`, `access_token`) are deprecated and will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
|
||||||
|
*
|
||||||
|
* @example <caption>Secure Authentication Example</caption>
|
||||||
|
* // Example authData for secure authentication:
|
||||||
|
* const authData = {
|
||||||
|
* github: {
|
||||||
|
* code: "abc123def456ghi789"
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @example <caption>Insecure Authentication Example (Not Recommended)</caption>
|
||||||
|
* // Example authData for insecure authentication:
|
||||||
|
* const authData = {
|
||||||
|
* github: {
|
||||||
|
* id: "1234567",
|
||||||
|
* access_token: "abc123def456ghi789" // Deprecated.
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @note `enableInsecureAuth` will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
|
||||||
|
* @note Secure authentication exchanges the `code` provided by the client for an access token using GitHub's OAuth API.
|
||||||
|
*
|
||||||
|
* @see {@link https://docs.github.com/en/developers/apps/authorizing-oauth-apps GitHub OAuth Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
|
||||||
function validateAuthData(authData) {
|
class GitHubAdapter extends BaseCodeAuthAdapter {
|
||||||
return request('user', authData.access_token).then(data => {
|
constructor() {
|
||||||
if (data && data.id == authData.id) {
|
super('GitHub');
|
||||||
return;
|
}
|
||||||
|
async getAccessTokenFromCode(authData) {
|
||||||
|
const tokenUrl = 'https://github.com/login/oauth/access_token';
|
||||||
|
const response = await fetch(tokenUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: this.clientId,
|
||||||
|
client_secret: this.clientSecret,
|
||||||
|
code: authData.code,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `Failed to exchange code for token: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Github auth is invalid for this user.');
|
|
||||||
});
|
const data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, data.error_description || data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserFromAccessToken(accessToken) {
|
||||||
|
const userApiUrl = 'https://api.github.com/user';
|
||||||
|
const response = await fetch(userApiUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `Failed to fetch GitHub user: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await response.json();
|
||||||
|
if (!userData.id || !userData.login) {
|
||||||
|
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Invalid GitHub user data received.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return userData;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this app id is valid.
|
export default new GitHubAdapter();
|
||||||
function validateAppId() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A promisey wrapper for api requests
|
|
||||||
function request(path, access_token) {
|
|
||||||
return httpsRequest.get({
|
|
||||||
host: 'api.github.com',
|
|
||||||
path: '/' + path,
|
|
||||||
headers: {
|
|
||||||
Authorization: 'bearer ' + access_token,
|
|
||||||
'User-Agent': 'parse-server',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAppId: validateAppId,
|
|
||||||
validateAuthData: validateAuthData,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,3 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Parse Server authentication adapter for Google.
|
||||||
|
*
|
||||||
|
* @class GoogleAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.clientId - Your Google application Client ID. Required for authentication.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Google authentication, use the following structure:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "google": {
|
||||||
|
* "clientId": "your-client-id"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **id**: The Google user ID.
|
||||||
|
* - **id_token**: The Google ID token.
|
||||||
|
* - **access_token**: The Google access token.
|
||||||
|
*
|
||||||
|
* ## Auth Payload
|
||||||
|
* ### Example Auth Data Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "google": {
|
||||||
|
* "id": "1234567",
|
||||||
|
* "id_token": "xxxxx.yyyyy.zzzzz",
|
||||||
|
* "access_token": "abc123def456ghi789"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - Ensure your Google Client ID is configured properly in the Parse Server configuration.
|
||||||
|
* - The `id_token` and `access_token` are validated against Google's authentication services.
|
||||||
|
*
|
||||||
|
* @see {@link https://developers.google.com/identity/sign-in/web/backend-auth Google Authentication Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Helper functions for accessing the google API.
|
// Helper functions for accessing the google API.
|
||||||
|
|||||||
@@ -1,33 +1,139 @@
|
|||||||
/* Google Play Game Services
|
/**
|
||||||
https://developers.google.com/games/services/web/api/players/get
|
* Parse Server authentication adapter for Google Play Games Services.
|
||||||
|
*
|
||||||
|
* @class GooglePlayGamesServicesAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.clientId - Your Google Play Games Services App Client ID. Required for secure authentication.
|
||||||
|
* @param {string} options.clientSecret - Your Google Play Games Services App Client Secret. Required for secure authentication.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Google Play Games Services authentication, use the following structure:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "gpgames": {
|
||||||
|
* "clientId": "your-client-id",
|
||||||
|
* "clientSecret": "your-client-secret"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Configuration (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "gpgames": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **Secure Authentication**: `code`, `redirect_uri`.
|
||||||
|
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Secure Authentication Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "gpgames": {
|
||||||
|
* "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
* "redirect_uri": "https://example.com/callback"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Insecure Authentication Payload (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "gpgames": {
|
||||||
|
* "id": "123456789",
|
||||||
|
* "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - `enableInsecureAuth` is **not recommended** and may be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
|
||||||
|
* - Secure authentication exchanges the `code` provided by the client for an access token using Google Play Games Services' OAuth API.
|
||||||
|
*
|
||||||
|
* @see {@link https://developers.google.com/games/services/console/enabling Google Play Games Services Authentication Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
const authData = {
|
import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
|
||||||
id: 'playerId',
|
class GooglePlayGamesServicesAdapter extends BaseCodeAuthAdapter {
|
||||||
access_token: 'token',
|
constructor() {
|
||||||
};
|
super("gpgames");
|
||||||
*/
|
|
||||||
const { Parse } = require('parse/node');
|
|
||||||
const httpsRequest = require('./httpsRequest');
|
|
||||||
|
|
||||||
// Returns a promise that fulfills if this user id is valid.
|
|
||||||
async function validateAuthData(authData) {
|
|
||||||
const response = await httpsRequest.get(
|
|
||||||
`https://www.googleapis.com/games/v1/players/${authData.id}?access_token=${authData.access_token}`
|
|
||||||
);
|
|
||||||
if (!(response && response.playerId === authData.id)) {
|
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
|
||||||
'Google Play Games Services - authData is invalid for this user.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAccessTokenFromCode(authData) {
|
||||||
|
const tokenUrl = 'https://oauth2.googleapis.com/token';
|
||||||
|
const response = await fetch(tokenUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: this.clientId,
|
||||||
|
client_secret: this.clientSecret,
|
||||||
|
code: authData.code,
|
||||||
|
redirect_uri: authData.redirectUri,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.VALIDATION_ERROR,
|
||||||
|
`Failed to exchange code for token: ${response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
data.error_description || data.error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserFromAccessToken(accessToken, authData) {
|
||||||
|
const userApiUrl = `https://www.googleapis.com/games/v1/players/${authData.id}`;
|
||||||
|
const response = await fetch(userApiUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.VALIDATION_ERROR,
|
||||||
|
`Failed to fetch Google Play Games Services user: ${response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await response.json();
|
||||||
|
if (!userData.playerId || userData.playerId !== authData.id) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.VALIDATION_ERROR,
|
||||||
|
'Invalid Google Play Games Services user data received.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: userData.playerId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills if this app id is valid.
|
export default new GooglePlayGamesServicesAdapter();
|
||||||
function validateAppId() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAppId,
|
|
||||||
validateAuthData,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -3,30 +3,31 @@ import Parse from 'parse/node';
|
|||||||
import AuthAdapter from './AuthAdapter';
|
import AuthAdapter from './AuthAdapter';
|
||||||
|
|
||||||
const apple = require('./apple');
|
const apple = require('./apple');
|
||||||
const gcenter = require('./gcenter');
|
|
||||||
const gpgames = require('./gpgames');
|
|
||||||
const facebook = require('./facebook');
|
|
||||||
const instagram = require('./instagram');
|
|
||||||
const linkedin = require('./linkedin');
|
|
||||||
const meetup = require('./meetup');
|
|
||||||
import mfa from './mfa';
|
|
||||||
const google = require('./google');
|
|
||||||
const github = require('./github');
|
|
||||||
const twitter = require('./twitter');
|
|
||||||
const spotify = require('./spotify');
|
|
||||||
const digits = require('./twitter'); // digits tokens are validated by twitter
|
const digits = require('./twitter'); // digits tokens are validated by twitter
|
||||||
const janrainengage = require('./janrainengage');
|
const facebook = require('./facebook');
|
||||||
|
import gcenter from './gcenter';
|
||||||
|
import github from './github';
|
||||||
|
const google = require('./google');
|
||||||
|
import gpgames from './gpgames';
|
||||||
|
import instagram from './instagram';
|
||||||
const janraincapture = require('./janraincapture');
|
const janraincapture = require('./janraincapture');
|
||||||
const line = require('./line');
|
const janrainengage = require('./janrainengage');
|
||||||
const vkontakte = require('./vkontakte');
|
|
||||||
const qq = require('./qq');
|
|
||||||
const wechat = require('./wechat');
|
|
||||||
const weibo = require('./weibo');
|
|
||||||
const oauth2 = require('./oauth2');
|
|
||||||
const phantauth = require('./phantauth');
|
|
||||||
const microsoft = require('./microsoft');
|
|
||||||
const keycloak = require('./keycloak');
|
const keycloak = require('./keycloak');
|
||||||
const ldap = require('./ldap');
|
const ldap = require('./ldap');
|
||||||
|
import line from './line';
|
||||||
|
import linkedin from './linkedin';
|
||||||
|
const meetup = require('./meetup');
|
||||||
|
import mfa from './mfa';
|
||||||
|
import microsoft from './microsoft';
|
||||||
|
import oauth2 from './oauth2';
|
||||||
|
const phantauth = require('./phantauth');
|
||||||
|
import qq from './qq';
|
||||||
|
import spotify from './spotify';
|
||||||
|
import twitter from './twitter';
|
||||||
|
const vkontakte = require('./vkontakte');
|
||||||
|
import wechat from './wechat';
|
||||||
|
import weibo from './weibo';
|
||||||
|
|
||||||
|
|
||||||
const anonymous = {
|
const anonymous = {
|
||||||
validateAuthData: () => {
|
validateAuthData: () => {
|
||||||
@@ -241,9 +242,9 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
|
|||||||
};
|
};
|
||||||
const result = afterFind.call(
|
const result = afterFind.call(
|
||||||
adapter,
|
adapter,
|
||||||
requestObject,
|
|
||||||
authData[provider],
|
authData[provider],
|
||||||
providerOptions
|
providerOptions,
|
||||||
|
requestObject,
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
authData[provider] = result;
|
authData[provider] = result;
|
||||||
|
|||||||
@@ -1,27 +1,121 @@
|
|||||||
// Helper functions for accessing the instagram API.
|
/**
|
||||||
var Parse = require('parse/node').Parse;
|
* Parse Server authentication adapter for Instagram.
|
||||||
const httpsRequest = require('./httpsRequest');
|
*
|
||||||
const defaultURL = 'https://graph.instagram.com/';
|
* @class InstagramAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.clientId - Your Instagram App Client ID. Required for secure authentication.
|
||||||
|
* @param {string} options.clientSecret - Your Instagram App Client Secret. Required for secure authentication.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Instagram authentication, use the following structure:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "instagram": {
|
||||||
|
* "clientId": "your-client-id",
|
||||||
|
* "clientSecret": "your-client-secret"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Configuration (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "instagram": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **Secure Authentication**: `code`, `redirect_uri`.
|
||||||
|
* - **Insecure Authentication (Deprecated)**: `id`, `access_token`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Secure Authentication Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "instagram": {
|
||||||
|
* "code": "lmn789opq012rst345uvw",
|
||||||
|
* "redirect_uri": "https://example.com/callback"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Insecure Authentication Payload (Deprecated)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "instagram": {
|
||||||
|
* "id": "1234567",
|
||||||
|
* "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - `enableInsecureAuth` is **deprecated** and will be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
|
||||||
|
* - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Instagram's OAuth flow.
|
||||||
|
*
|
||||||
|
* @see {@link https://developers.facebook.com/docs/instagram-basic-display-api/getting-started Instagram Basic Display API - Getting Started}
|
||||||
|
*/
|
||||||
|
|
||||||
// Returns a promise that fulfills if this user id is valid.
|
|
||||||
function validateAuthData(authData) {
|
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
|
||||||
const apiURL = authData.apiURL || defaultURL;
|
class InstagramAdapter extends BaseAuthCodeAdapter {
|
||||||
const path = `${apiURL}me?fields=id&access_token=${authData.access_token}`;
|
constructor() {
|
||||||
return httpsRequest.get(path).then(response => {
|
super('Instagram');
|
||||||
const user = response.data ? response.data : response;
|
}
|
||||||
if (user && user.id == authData.id) {
|
|
||||||
return;
|
async getAccessTokenFromCode(authData) {
|
||||||
|
const response = await fetch('https://api.instagram.com/oauth/access_token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: this.clientId,
|
||||||
|
client_secret: this.clientSecret,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri: this.redirectUri,
|
||||||
|
code: authData.code
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.');
|
||||||
}
|
}
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram auth is invalid for this user.');
|
|
||||||
});
|
const data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, data.error_description || data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserFromAccessToken(accessToken, authData) {
|
||||||
|
const defaultURL = 'https://graph.instagram.com/';
|
||||||
|
const apiURL = authData.apiURL || defaultURL;
|
||||||
|
const path = `${apiURL}me?fields=id&access_token=${accessToken}`;
|
||||||
|
|
||||||
|
const response = await fetch(path);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await response.json();
|
||||||
|
if (user?.id !== authData.id) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram auth is invalid for this user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this app id is valid.
|
export default new InstagramAdapter();
|
||||||
function validateAppId() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAppId,
|
|
||||||
validateAuthData,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,3 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Parse Server authentication adapter for Janrain Capture API.
|
||||||
|
*
|
||||||
|
* @class JanrainCapture
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {String} options.janrain_capture_host - The Janrain Capture API host.
|
||||||
|
*
|
||||||
|
* @param {Object} authData - The authentication data provided by the client.
|
||||||
|
* @param {String} authData.id - The Janrain Capture user ID.
|
||||||
|
* @param {String} authData.access_token - The Janrain Capture access token.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Janrain Capture authentication, use the following structure:
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "janrain": {
|
||||||
|
* "janrain_capture_host": "your-janrain-capture-host"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - `id`: The Janrain Capture user ID.
|
||||||
|
* - `access_token`: An authorized Janrain Capture access token for the user.
|
||||||
|
*
|
||||||
|
* ## Auth Payload Example
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "janrain": {
|
||||||
|
* "id": "user's Janrain Capture ID as a string",
|
||||||
|
* "access_token": "an authorized Janrain Capture access token for the user"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* Parse Server validates the provided `authData` using the Janrain Capture API.
|
||||||
|
*
|
||||||
|
* @see {@link https://docs.janrain.com/api/registration/entity/#entity Janrain Capture API Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
// Helper functions for accessing the Janrain Capture API.
|
// Helper functions for accessing the Janrain Capture API.
|
||||||
var Parse = require('parse/node').Parse;
|
var Parse = require('parse/node').Parse;
|
||||||
var querystring = require('querystring');
|
var querystring = require('querystring');
|
||||||
|
|||||||
@@ -2,9 +2,18 @@
|
|||||||
var httpsRequest = require('./httpsRequest');
|
var httpsRequest = require('./httpsRequest');
|
||||||
var Parse = require('parse/node').Parse;
|
var Parse = require('parse/node').Parse;
|
||||||
var querystring = require('querystring');
|
var querystring = require('querystring');
|
||||||
|
import Config from '../../Config';
|
||||||
|
import Deprecator from '../../Deprecator/Deprecator';
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
function validateAuthData(authData, options) {
|
function validateAuthData(authData, options) {
|
||||||
|
const config = Config.get(Parse.applicationId);
|
||||||
|
|
||||||
|
Deprecator.logRuntimeDeprecation({ usage: 'janrainengage adapter' });
|
||||||
|
if (!config?.auth?.janrainengage?.enableInsecureAuth || !config.enableInsecureAuthAdapters) {
|
||||||
|
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'janrainengage adapter only works with enableInsecureAuth: true');
|
||||||
|
}
|
||||||
|
|
||||||
return apiRequest(options.api_key, authData.auth_token).then(data => {
|
return apiRequest(options.api_key, authData.auth_token).then(data => {
|
||||||
//successful response will have a "stat" (status) of 'ok' and a profile node with an identifier
|
//successful response will have a "stat" (status) of 'ok' and a profile node with an identifier
|
||||||
//see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data
|
//see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data
|
||||||
|
|||||||
@@ -1,37 +1,70 @@
|
|||||||
/*
|
/**
|
||||||
# Parse Server Keycloak Authentication
|
* Parse Server authentication adapter for Keycloak.
|
||||||
|
*
|
||||||
## Keycloak `authData`
|
* @class KeycloakAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
```
|
* @param {Object} options.config - The Keycloak configuration object, typically loaded from a JSON file.
|
||||||
{
|
* @param {String} options.config.auth-server-url - The Keycloak authentication server URL.
|
||||||
"keycloak": {
|
* @param {String} options.config.realm - The Keycloak realm name.
|
||||||
"access_token": "access token you got from keycloak JS client authentication",
|
* @param {String} options.config.client-id - The Keycloak client ID.
|
||||||
"id": "the id retrieved from client authentication in Keycloak",
|
*
|
||||||
"roles": ["the roles retrieved from client authentication in Keycloak"],
|
* @param {Object} authData - The authentication data provided by the client.
|
||||||
"groups": ["the groups retrieved from client authentication in Keycloak"]
|
* @param {String} authData.access_token - The Keycloak access token retrieved during client authentication.
|
||||||
}
|
* @param {String} authData.id - The user ID retrieved from Keycloak during client authentication.
|
||||||
}
|
* @param {Array} [authData.roles] - The roles assigned to the user in Keycloak (optional).
|
||||||
```
|
* @param {Array} [authData.groups] - The groups assigned to the user in Keycloak (optional).
|
||||||
|
*
|
||||||
The authentication module will test if the authData is the same as the
|
* @description
|
||||||
userinfo oauth call, comparing the attributes
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Keycloak authentication, use the following structure:
|
||||||
Copy the JSON config file generated on Keycloak (https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
|
* ```javascript
|
||||||
and paste it inside of a folder (Ex.: `auth/keycloak.json`) in your server.
|
* {
|
||||||
|
* "auth": {
|
||||||
The options passed to Parse server:
|
* "keycloak": {
|
||||||
|
* "config": require('./auth/keycloak.json')
|
||||||
```
|
* }
|
||||||
{
|
* }
|
||||||
auth: {
|
* }
|
||||||
keycloak: {
|
* ```
|
||||||
config: require(`./auth/keycloak.json`)
|
* Ensure the `keycloak.json` configuration file is generated from Keycloak's setup guide and includes:
|
||||||
}
|
* - `auth-server-url`: The Keycloak authentication server URL.
|
||||||
}
|
* - `realm`: The Keycloak realm name.
|
||||||
}
|
* - `client-id`: The Keycloak client ID.
|
||||||
```
|
*
|
||||||
*/
|
* ## Auth Data
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - `access_token`: The Keycloak access token retrieved during client authentication.
|
||||||
|
* - `id`: The user ID retrieved from Keycloak during client authentication.
|
||||||
|
* - `roles` (optional): The roles assigned to the user in Keycloak.
|
||||||
|
* - `groups` (optional): The groups assigned to the user in Keycloak.
|
||||||
|
*
|
||||||
|
* ## Auth Payload Example
|
||||||
|
* ### Example Auth Data
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "keycloak": {
|
||||||
|
* "access_token": "an authorized Keycloak access token for the user",
|
||||||
|
* "id": "user's Keycloak ID as a string",
|
||||||
|
* "roles": ["admin", "user"],
|
||||||
|
* "groups": ["group1", "group2"]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - Parse Server validates the provided `authData` by making a `userinfo` call to Keycloak and ensures the attributes match those returned by Keycloak.
|
||||||
|
*
|
||||||
|
* ## Keycloak Configuration
|
||||||
|
* To configure Keycloak, copy the JSON configuration file generated from Keycloak's setup guide:
|
||||||
|
* - [Keycloak Securing Apps Documentation](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
|
||||||
|
*
|
||||||
|
* Place the configuration file on your server, for example:
|
||||||
|
* - `auth/keycloak.json`
|
||||||
|
*
|
||||||
|
* For more information on Keycloak authentication, see:
|
||||||
|
* - [Securing Apps Documentation](https://www.keycloak.org/docs/latest/securing_apps/)
|
||||||
|
* - [Server Administration Documentation](https://www.keycloak.org/docs/latest/server_admin/)
|
||||||
|
*/
|
||||||
|
|
||||||
const { Parse } = require('parse/node');
|
const { Parse } = require('parse/node');
|
||||||
const httpsRequest = require('./httpsRequest');
|
const httpsRequest = require('./httpsRequest');
|
||||||
|
|||||||
@@ -1,3 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Parse Server authentication adapter for LDAP.
|
||||||
|
*
|
||||||
|
* @class LDAP
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {String} options.url - The LDAP server URL. Must start with `ldap://` or `ldaps://`.
|
||||||
|
* @param {String} options.suffix - The LDAP suffix for user distinguished names (DN).
|
||||||
|
* @param {String} [options.dn] - The distinguished name (DN) template for user authentication. Replace `{{id}}` with the username.
|
||||||
|
* @param {Object} [options.tlsOptions] - Options for LDAPS TLS connections.
|
||||||
|
* @param {String} [options.groupCn] - The common name (CN) of the group to verify user membership.
|
||||||
|
* @param {String} [options.groupFilter] - The LDAP search filter for groups, with `{{id}}` replaced by the username.
|
||||||
|
*
|
||||||
|
* @param {Object} authData - The authentication data provided by the client.
|
||||||
|
* @param {String} authData.id - The user's LDAP username.
|
||||||
|
* @param {String} authData.password - The user's LDAP password.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for LDAP authentication, use the following structure:
|
||||||
|
* ```javascript
|
||||||
|
* {
|
||||||
|
* auth: {
|
||||||
|
* ldap: {
|
||||||
|
* url: 'ldaps://ldap.example.com',
|
||||||
|
* suffix: 'ou=users,dc=example,dc=com',
|
||||||
|
* groupCn: 'admins',
|
||||||
|
* groupFilter: '(memberUid={{id}})',
|
||||||
|
* tlsOptions: {
|
||||||
|
* rejectUnauthorized: false
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Authentication Process
|
||||||
|
* 1. Validates the provided `authData` using an LDAP bind operation.
|
||||||
|
* 2. Optionally, verifies that the user belongs to a specific group by performing an LDAP search using the provided `groupCn` or `groupFilter`.
|
||||||
|
*
|
||||||
|
* ## Auth Payload
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - `id`: The user's LDAP username.
|
||||||
|
* - `password`: The user's LDAP password.
|
||||||
|
*
|
||||||
|
* ### Example Auth Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "ldap": {
|
||||||
|
* "id": "jdoe",
|
||||||
|
* "password": "password123"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example <caption>Configuration Example</caption>
|
||||||
|
* // Example Parse Server configuration:
|
||||||
|
* const config = {
|
||||||
|
* auth: {
|
||||||
|
* ldap: {
|
||||||
|
* url: 'ldaps://ldap.example.com',
|
||||||
|
* suffix: 'ou=users,dc=example,dc=com',
|
||||||
|
* groupCn: 'admins',
|
||||||
|
* groupFilter: '(memberUid={{id}})',
|
||||||
|
* tlsOptions: {
|
||||||
|
* rejectUnauthorized: false
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @see {@link https://ldap.com/ LDAP Basics}
|
||||||
|
* @see {@link https://ldap.com/ldap-filters/ LDAP Filters}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
const ldapjs = require('ldapjs');
|
const ldapjs = require('ldapjs');
|
||||||
const Parse = require('parse/node').Parse;
|
const Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,143 @@
|
|||||||
// Helper functions for accessing the line API.
|
/**
|
||||||
var Parse = require('parse/node').Parse;
|
* Parse Server authentication adapter for Line.
|
||||||
const httpsRequest = require('./httpsRequest');
|
*
|
||||||
|
* @class LineAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.clientId - Your Line App Client ID. Required for secure authentication.
|
||||||
|
* @param {string} options.clientSecret - Your Line App Client Secret. Required for secure authentication.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Line authentication, use the following structure:
|
||||||
|
* ### Secure Configuration
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "line": {
|
||||||
|
* "clientId": "your-client-id",
|
||||||
|
* "clientSecret": "your-client-secret"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Configuration (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "line": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **Secure Authentication**: `code`, `redirect_uri`.
|
||||||
|
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Secure Authentication Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "line": {
|
||||||
|
* "code": "xxxxxxxxx",
|
||||||
|
* "redirect_uri": "https://example.com/callback"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Insecure Authentication Payload (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "line": {
|
||||||
|
* "id": "1234567",
|
||||||
|
* "access_token": "xxxxxxxxx"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - `enableInsecureAuth` is **not recommended** and will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
|
||||||
|
* - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Line's OAuth flow.
|
||||||
|
*
|
||||||
|
* @see {@link https://developers.line.biz/en/docs/line-login/integrate-line-login/ Line Login Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
// Returns a promise that fulfills if this user id is valid.
|
import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
|
||||||
function validateAuthData(authData) {
|
|
||||||
return request('profile', authData.access_token).then(response => {
|
class LineAdapter extends BaseCodeAuthAdapter {
|
||||||
if (response && response.userId && response.userId === authData.id) {
|
constructor() {
|
||||||
return;
|
super('Line');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessTokenFromCode(authData) {
|
||||||
|
if (!authData.code) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Line auth is invalid for this user.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Line auth is invalid for this user.');
|
|
||||||
});
|
const tokenUrl = 'https://api.line.me/oauth2/v2.1/token';
|
||||||
|
const response = await fetch(tokenUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: this.clientId,
|
||||||
|
client_secret: this.clientSecret,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri: authData.redirect_uri,
|
||||||
|
code: authData.code,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
`Failed to exchange code for token: ${response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
data.error_description || data.error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserFromAccessToken(accessToken) {
|
||||||
|
const userApiUrl = 'https://api.line.me/v2/profile';
|
||||||
|
const response = await fetch(userApiUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
`Failed to fetch Line user: ${response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await response.json();
|
||||||
|
if (!userData?.userId) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.VALIDATION_ERROR,
|
||||||
|
'Invalid Line user data received.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this app id is valid.
|
export default new LineAdapter();
|
||||||
function validateAppId() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A promisey wrapper for api requests
|
|
||||||
function request(path, access_token) {
|
|
||||||
var options = {
|
|
||||||
host: 'api.line.me',
|
|
||||||
path: '/v2/' + path,
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + access_token,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return httpsRequest.get(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAppId: validateAppId,
|
|
||||||
validateAuthData: validateAuthData,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,40 +1,115 @@
|
|||||||
// Helper functions for accessing the linkedin API.
|
/**
|
||||||
var Parse = require('parse/node').Parse;
|
* Parse Server authentication adapter for LinkedIn.
|
||||||
const httpsRequest = require('./httpsRequest');
|
*
|
||||||
|
* @class LinkedInAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.clientId - Your LinkedIn App Client ID. Required for secure authentication.
|
||||||
|
* @param {string} options.clientSecret - Your LinkedIn App Client Secret. Required for secure authentication.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for LinkedIn authentication, use the following structure:
|
||||||
|
* ### Secure Configuration
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "linkedin": {
|
||||||
|
* "clientId": "your-client-id",
|
||||||
|
* "clientSecret": "your-client-secret"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Configuration (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "linkedin": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **Secure Authentication**: `code`, `redirect_uri`, and optionally `is_mobile_sdk`.
|
||||||
|
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`, and optionally `is_mobile_sdk`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Secure Authentication Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "linkedin": {
|
||||||
|
* "code": "lmn789opq012rst345uvw",
|
||||||
|
* "redirect_uri": "https://your-redirect-uri.com/callback",
|
||||||
|
* "is_mobile_sdk": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Insecure Authentication Payload (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "linkedin": {
|
||||||
|
* "id": "7654321",
|
||||||
|
* "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc",
|
||||||
|
* "is_mobile_sdk": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using LinkedIn's OAuth API.
|
||||||
|
* - Insecure authentication validates the user ID and access token directly, bypassing OAuth flows. This method is **not recommended** and may introduce security vulnerabilities.
|
||||||
|
* - `enableInsecureAuth` is **deprecated** and may be removed in future versions.
|
||||||
|
*
|
||||||
|
* @see {@link https://learn.microsoft.com/en-us/linkedin/shared/authentication/authentication LinkedIn Authentication Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
|
||||||
function validateAuthData(authData) {
|
class LinkedInAdapter extends BaseAuthCodeAdapter {
|
||||||
return request('me', authData.access_token, authData.is_mobile_sdk).then(data => {
|
constructor() {
|
||||||
if (data && data.id == authData.id) {
|
super('LinkedIn');
|
||||||
return;
|
}
|
||||||
}
|
async getUserFromAccessToken(access_token, authData) {
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Linkedin auth is invalid for this user.');
|
const response = await fetch('https://api.linkedin.com/v2/me', {
|
||||||
});
|
headers: {
|
||||||
}
|
Authorization: `Bearer ${access_token}`,
|
||||||
|
'x-li-format': 'json',
|
||||||
// Returns a promise that fulfills iff this app id is valid.
|
'x-li-src': authData?.is_mobile_sdk ? 'msdk' : undefined,
|
||||||
function validateAppId() {
|
},
|
||||||
return Promise.resolve();
|
});
|
||||||
}
|
|
||||||
|
if (!response.ok) {
|
||||||
// A promisey wrapper for api requests
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn API request failed.');
|
||||||
function request(path, access_token, is_mobile_sdk) {
|
}
|
||||||
var headers = {
|
|
||||||
Authorization: 'Bearer ' + access_token,
|
return response.json();
|
||||||
'x-li-format': 'json',
|
}
|
||||||
};
|
|
||||||
|
async getAccessTokenFromCode(authData) {
|
||||||
if (is_mobile_sdk) {
|
const response = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
|
||||||
headers['x-li-src'] = 'msdk';
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: authData.code,
|
||||||
|
redirect_uri: authData.redirect_uri,
|
||||||
|
client_id: this.clientId,
|
||||||
|
client_secret: this.clientSecret,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn API request failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
return json.access_token;
|
||||||
}
|
}
|
||||||
return httpsRequest.get({
|
|
||||||
host: 'api.linkedin.com',
|
|
||||||
path: '/v2/' + path,
|
|
||||||
headers: headers,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export default new LinkedInAdapter();
|
||||||
validateAppId: validateAppId,
|
|
||||||
validateAuthData: validateAuthData,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
// Helper functions for accessing the meetup API.
|
// Helper functions for accessing the meetup API.
|
||||||
var Parse = require('parse/node').Parse;
|
var Parse = require('parse/node').Parse;
|
||||||
const httpsRequest = require('./httpsRequest');
|
const httpsRequest = require('./httpsRequest');
|
||||||
|
import Config from '../../Config';
|
||||||
|
import Deprecator from '../../Deprecator/Deprecator';
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
function validateAuthData(authData) {
|
async function validateAuthData(authData) {
|
||||||
return request('member/self', authData.access_token).then(data => {
|
const config = Config.get(Parse.applicationId);
|
||||||
if (data && data.id == authData.id) {
|
const meetupConfig = config.auth.meetup;
|
||||||
return;
|
|
||||||
}
|
Deprecator.logRuntimeDeprecation({ usage: 'meetup adapter' });
|
||||||
|
|
||||||
|
if (!meetupConfig?.enableInsecureAuth) {
|
||||||
|
throw new Parse.Error('Meetup only works with enableInsecureAuth: true');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await request('member/self', authData.access_token);
|
||||||
|
if (data?.id !== authData.id) {
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Meetup auth is invalid for this user.');
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Meetup auth is invalid for this user.');
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this app id is valid.
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
|
|||||||
@@ -1,3 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Parse Server authentication adapter for Multi-Factor Authentication (MFA).
|
||||||
|
*
|
||||||
|
* @class MFAAdapter
|
||||||
|
* @param {Object} options - The adapter options.
|
||||||
|
* @param {Array<String>} options.options - Supported MFA methods. Must include `"SMS"` or `"TOTP"`.
|
||||||
|
* @param {Number} [options.digits=6] - The number of digits for the one-time password (OTP). Must be between 4 and 10.
|
||||||
|
* @param {Number} [options.period=30] - The validity period of the OTP in seconds. Must be greater than 10.
|
||||||
|
* @param {String} [options.algorithm="SHA1"] - The algorithm used for TOTP generation. Defaults to `"SHA1"`.
|
||||||
|
* @param {Function} [options.sendSMS] - A callback function for sending SMS OTPs. Required if `"SMS"` is included in `options`.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for MFA, use the following structure:
|
||||||
|
* ```javascript
|
||||||
|
* {
|
||||||
|
* auth: {
|
||||||
|
* mfa: {
|
||||||
|
* options: ["SMS", "TOTP"],
|
||||||
|
* digits: 6,
|
||||||
|
* period: 30,
|
||||||
|
* algorithm: "SHA1",
|
||||||
|
* sendSMS: (token, mobile) => {
|
||||||
|
* // Send the SMS using your preferred SMS provider.
|
||||||
|
* console.log(`Sending SMS to ${mobile} with token: ${token}`);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## MFA Methods
|
||||||
|
* - **SMS**:
|
||||||
|
* - Requires a valid mobile number.
|
||||||
|
* - Sends a one-time password (OTP) via SMS for login or verification.
|
||||||
|
* - Uses the `sendSMS` callback for sending the OTP.
|
||||||
|
*
|
||||||
|
* - **TOTP**:
|
||||||
|
* - Requires a secret key for setup.
|
||||||
|
* - Validates the user's OTP against a time-based one-time password (TOTP) generated using the secret key.
|
||||||
|
* - Supports configurable digits, period, and algorithm for TOTP generation.
|
||||||
|
*
|
||||||
|
* ## MFA Payload
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **For SMS-based MFA**:
|
||||||
|
* - `mobile`: The user's mobile number (required for setup).
|
||||||
|
* - `token`: The OTP provided by the user for login or verification.
|
||||||
|
* - **For TOTP-based MFA**:
|
||||||
|
* - `secret`: The TOTP secret key for the user (required for setup).
|
||||||
|
* - `token`: The OTP provided by the user for login or verification.
|
||||||
|
*
|
||||||
|
* ## Example Payloads
|
||||||
|
* ### SMS Setup Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "mobile": "+1234567890"
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### TOTP Setup Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "secret": "BASE32ENCODEDSECRET",
|
||||||
|
* "token": "123456"
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Login Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "token": "123456"
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @see {@link https://en.wikipedia.org/wiki/Time-based_One-Time_Password_algorithm Time-based One-Time Password Algorithm (TOTP)}
|
||||||
|
* @see {@link https://tools.ietf.org/html/rfc6238 RFC 6238: TOTP: Time-Based One-Time Password Algorithm}
|
||||||
|
*/
|
||||||
|
|
||||||
import { TOTP, Secret } from 'otpauth';
|
import { TOTP, Secret } from 'otpauth';
|
||||||
import { randomString } from '../../cryptoUtils';
|
import { randomString } from '../../cryptoUtils';
|
||||||
import AuthAdapter from './AuthAdapter';
|
import AuthAdapter from './AuthAdapter';
|
||||||
@@ -113,7 +191,7 @@ class MFAAdapter extends AuthAdapter {
|
|||||||
}
|
}
|
||||||
throw 'Invalid MFA data';
|
throw 'Invalid MFA data';
|
||||||
}
|
}
|
||||||
afterFind(req, authData) {
|
afterFind(authData, options, req) {
|
||||||
if (req.master) {
|
if (req.master) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,109 @@
|
|||||||
// Helper functions for accessing the microsoft graph API.
|
/**
|
||||||
var Parse = require('parse/node').Parse;
|
* Parse Server authentication adapter for Microsoft.
|
||||||
const httpsRequest = require('./httpsRequest');
|
*
|
||||||
|
* @class MicrosoftAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.clientId - Your Microsoft App Client ID. Required for secure authentication.
|
||||||
|
* @param {string} options.clientSecret - Your Microsoft App Client Secret. Required for secure authentication.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Microsoft authentication, use the following structure:
|
||||||
|
* ### Secure Configuration
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "microsoft": {
|
||||||
|
* "clientId": "your-client-id",
|
||||||
|
* "clientSecret": "your-client-secret"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Configuration (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "microsoft": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **Secure Authentication**: `code`, `redirect_uri`.
|
||||||
|
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Secure Authentication Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "microsoft": {
|
||||||
|
* "code": "lmn789opq012rst345uvw",
|
||||||
|
* "redirect_uri": "https://your-redirect-uri.com/callback"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Authentication Payload (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "microsoft": {
|
||||||
|
* "id": "7654321",
|
||||||
|
* "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Microsoft's OAuth API.
|
||||||
|
* - **Insecure authentication** validates the user ID and access token directly, bypassing OAuth flows (not recommended). This method is deprecated and may be removed in future versions.
|
||||||
|
*
|
||||||
|
* @see {@link https://docs.microsoft.com/en-us/graph/auth/auth-concepts Microsoft Authentication Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
// Returns a promise that fulfills if this user mail is valid.
|
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
|
||||||
function validateAuthData(authData) {
|
class MicrosoftAdapter extends BaseAuthCodeAdapter {
|
||||||
return request('me', authData.access_token).then(response => {
|
constructor() {
|
||||||
if (response && response.id && response.id == authData.id) {
|
super('Microsoft');
|
||||||
return;
|
}
|
||||||
|
async getUserFromAccessToken(access_token) {
|
||||||
|
const userResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + access_token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userResponse.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Microsoft API request failed.');
|
||||||
}
|
}
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
return userResponse.json();
|
||||||
'Microsoft Graph auth is invalid for this user.'
|
}
|
||||||
);
|
|
||||||
});
|
async getAccessTokenFromCode(authData) {
|
||||||
|
const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: this.clientId,
|
||||||
|
client_secret: this.clientSecret,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri: authData.redirect_uri,
|
||||||
|
code: authData.code,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Microsoft API request failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
return json.access_token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills if this app id is valid.
|
export default new MicrosoftAdapter();
|
||||||
function validateAppId() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A promisey wrapper for api requests
|
|
||||||
function request(path, access_token) {
|
|
||||||
return httpsRequest.get({
|
|
||||||
host: 'graph.microsoft.com',
|
|
||||||
path: '/v1.0/' + path,
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + access_token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAppId: validateAppId,
|
|
||||||
validateAuthData: validateAuthData,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,137 +1,121 @@
|
|||||||
/*
|
/**
|
||||||
* This auth adapter is based on the OAuth 2.0 Token Introspection specification.
|
* Parse Server authentication adapter for OAuth2 Token Introspection.
|
||||||
* See RFC 7662 for details (https://tools.ietf.org/html/rfc7662).
|
|
||||||
* It's purpose is to validate OAuth2 access tokens using the OAuth2 provider's
|
|
||||||
* token introspection endpoint (if implemented by the provider).
|
|
||||||
*
|
*
|
||||||
* The adapter accepts the following config parameters:
|
* @class OAuth2Adapter
|
||||||
*
|
* @param {Object} options - The adapter configuration options.
|
||||||
* 1. "tokenIntrospectionEndpointUrl" (string, required)
|
* @param {string} options.tokenIntrospectionEndpointUrl - The URL of the token introspection endpoint. Required.
|
||||||
* The URL of the token introspection endpoint of the OAuth2 provider that
|
* @param {boolean} options.oauth2 - Indicates that the request should be handled by the OAuth2 adapter. Required.
|
||||||
* issued the access token to the client that is to be validated.
|
* @param {string} [options.useridField] - The field in the introspection response that contains the user ID. Optional.
|
||||||
*
|
* @param {string} [options.appidField] - The field in the introspection response that contains the app ID. Optional.
|
||||||
* 2. "useridField" (string, optional)
|
* @param {string[]} [options.appIds] - List of allowed app IDs. Required if `appidField` is defined.
|
||||||
* The name of the field in the token introspection response that contains
|
* @param {string} [options.authorizationHeader] - The Authorization header value for the introspection request. Optional.
|
||||||
* the userid. If specified, it will be used to verify the value of the "id"
|
|
||||||
* field in the "authData" JSON that is coming from the client.
|
|
||||||
* This can be the "aud" (i.e. audience), the "sub" (i.e. subject) or the
|
|
||||||
* "username" field in the introspection response, but since only the
|
|
||||||
* "active" field is required and all other reponse fields are optional
|
|
||||||
* in the RFC, it has to be optional in this adapter as well.
|
|
||||||
* Default: - (undefined)
|
|
||||||
*
|
|
||||||
* 3. "appidField" (string, optional)
|
|
||||||
* The name of the field in the token introspection response that contains
|
|
||||||
* the appId of the client. If specified, it will be used to verify it's
|
|
||||||
* value against the set of appIds in the adapter config. The concept of
|
|
||||||
* appIds comes from the two major social login providers
|
|
||||||
* (Google and Facebook). They have not yet implemented the token
|
|
||||||
* introspection endpoint, but the concept can be valid for any OAuth2
|
|
||||||
* provider.
|
|
||||||
* Default: - (undefined)
|
|
||||||
*
|
|
||||||
* 4. "appIds" (array of strings, required if appidField is defined)
|
|
||||||
* A set of appIds that are used to restrict accepted access tokens based
|
|
||||||
* on a specific field's value in the token introspection response.
|
|
||||||
* Default: - (undefined)
|
|
||||||
*
|
|
||||||
* 5. "authorizationHeader" (string, optional)
|
|
||||||
* The value of the "Authorization" HTTP header in requests sent to the
|
|
||||||
* introspection endpoint. It must contain the raw value.
|
|
||||||
* Thus if HTTP Basic authorization is to be used, it must contain the
|
|
||||||
* "Basic" string, followed by whitespace, then by the base64 encoded
|
|
||||||
* version of the concatenated <username> + ":" + <password> string.
|
|
||||||
* Eg. "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
|
|
||||||
*
|
|
||||||
* The adapter expects requests with the following authData JSON:
|
|
||||||
*
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for OAuth2 Token Introspection, use the following structure:
|
||||||
|
* ```json
|
||||||
* {
|
* {
|
||||||
* "someadapter": {
|
* "auth": {
|
||||||
* "id": "user's OAuth2 provider-specific id as a string",
|
* "oauth2Provider": {
|
||||||
* "access_token": "an authorized OAuth2 access token for the user",
|
* "tokenIntrospectionEndpointUrl": "https://provider.com/introspect",
|
||||||
|
* "useridField": "sub",
|
||||||
|
* "appidField": "aud",
|
||||||
|
* "appIds": ["my-app-id"],
|
||||||
|
* "authorizationHeader": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
|
||||||
|
* "oauth2": true
|
||||||
|
* }
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - `id`: The user ID provided by the client.
|
||||||
|
* - `access_token`: The access token provided by the client.
|
||||||
|
*
|
||||||
|
* ## Auth Payload
|
||||||
|
* ### Example Auth Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "oauth2": {
|
||||||
|
* "id": "user-id",
|
||||||
|
* "access_token": "access-token"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - `tokenIntrospectionEndpointUrl` is mandatory and should point to a valid OAuth2 provider's introspection endpoint.
|
||||||
|
* - If `appidField` is defined, `appIds` must also be specified to validate the app ID in the introspection response.
|
||||||
|
* - `authorizationHeader` can be used to authenticate requests to the token introspection endpoint.
|
||||||
|
*
|
||||||
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc7662 OAuth 2.0 Token Introspection Specification}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Parse = require('parse/node').Parse;
|
|
||||||
const querystring = require('querystring');
|
|
||||||
const httpsRequest = require('./httpsRequest');
|
|
||||||
|
|
||||||
const INVALID_ACCESS = 'OAuth2 access token is invalid for this user.';
|
import AuthAdapter from './AuthAdapter';
|
||||||
const INVALID_ACCESS_APPID =
|
|
||||||
"OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration.";
|
|
||||||
const MISSING_APPIDS =
|
|
||||||
'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).';
|
|
||||||
const MISSING_URL = 'OAuth2 token introspection endpoint URL is missing from configuration!';
|
|
||||||
|
|
||||||
// Returns a promise that fulfills if this user id is valid.
|
class OAuth2Adapter extends AuthAdapter {
|
||||||
function validateAuthData(authData, options) {
|
validateOptions(options) {
|
||||||
return requestTokenInfo(options, authData.access_token).then(response => {
|
super.validateOptions(options);
|
||||||
if (
|
|
||||||
!response ||
|
if (!options.tokenIntrospectionEndpointUrl) {
|
||||||
!response.active ||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.');
|
||||||
(options.useridField && authData.id !== response[options.useridField])
|
}
|
||||||
) {
|
if (options.appidField && !options.appIds?.length) {
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS);
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 configuration is missing app IDs.');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateAppId(appIds, authData, options) {
|
this.tokenIntrospectionEndpointUrl = options.tokenIntrospectionEndpointUrl;
|
||||||
if (!options || !options.appidField) {
|
this.useridField = options.useridField;
|
||||||
return Promise.resolve();
|
this.appidField = options.appidField;
|
||||||
|
this.appIds = options.appIds;
|
||||||
|
this.authorizationHeader = options.authorizationHeader;
|
||||||
}
|
}
|
||||||
if (!appIds || appIds.length === 0) {
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_APPIDS);
|
async validateAppId(authData) {
|
||||||
}
|
if (!this.appidField) {
|
||||||
return requestTokenInfo(options, authData.access_token).then(response => {
|
|
||||||
if (!response || !response.active) {
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS);
|
|
||||||
}
|
|
||||||
const appidField = options.appidField;
|
|
||||||
if (!response[appidField]) {
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS_APPID);
|
|
||||||
}
|
|
||||||
const responseValue = response[appidField];
|
|
||||||
if (!Array.isArray(responseValue) && appIds.includes(responseValue)) {
|
|
||||||
return;
|
return;
|
||||||
} else if (
|
|
||||||
Array.isArray(responseValue) &&
|
|
||||||
responseValue.some(appId => appIds.includes(appId))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS_APPID);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const response = await this.requestTokenInfo(authData.access_token);
|
||||||
|
|
||||||
|
const appIdFieldValue = response[this.appidField];
|
||||||
|
const isValidAppId = Array.isArray(appIdFieldValue)
|
||||||
|
? appIdFieldValue.some(appId => this.appIds.includes(appId))
|
||||||
|
: this.appIds.includes(appIdFieldValue);
|
||||||
|
|
||||||
|
if (!isValidAppId) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2: Invalid app ID.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAuthData(authData) {
|
||||||
|
const response = await this.requestTokenInfo(authData.access_token);
|
||||||
|
|
||||||
|
if (!response.active || (this.useridField && authData.id !== response[this.useridField])) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestTokenInfo(accessToken) {
|
||||||
|
const response = await fetch(this.tokenIntrospectionEndpointUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
...(this.authorizationHeader && { Authorization: this.authorizationHeader })
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ token: accessToken })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection request failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A promise wrapper for requests to the OAuth2 token introspection endpoint.
|
export default new OAuth2Adapter();
|
||||||
function requestTokenInfo(options, access_token) {
|
|
||||||
if (!options || !options.tokenIntrospectionEndpointUrl) {
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_URL);
|
|
||||||
}
|
|
||||||
const parsedUrl = new URL(options.tokenIntrospectionEndpointUrl);
|
|
||||||
const postData = querystring.stringify({
|
|
||||||
token: access_token,
|
|
||||||
});
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Content-Length': Buffer.byteLength(postData),
|
|
||||||
};
|
|
||||||
if (options.authorizationHeader) {
|
|
||||||
headers['Authorization'] = options.authorizationHeader;
|
|
||||||
}
|
|
||||||
const postOptions = {
|
|
||||||
hostname: parsedUrl.hostname,
|
|
||||||
path: parsedUrl.pathname,
|
|
||||||
method: 'POST',
|
|
||||||
headers: headers,
|
|
||||||
};
|
|
||||||
return httpsRequest.request(postOptions, postData);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAppId: validateAppId,
|
|
||||||
validateAuthData: validateAuthData,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -7,15 +7,24 @@
|
|||||||
|
|
||||||
const { Parse } = require('parse/node');
|
const { Parse } = require('parse/node');
|
||||||
const httpsRequest = require('./httpsRequest');
|
const httpsRequest = require('./httpsRequest');
|
||||||
|
import Config from '../../Config';
|
||||||
|
import Deprecator from '../../Deprecator/Deprecator';
|
||||||
|
|
||||||
// Returns a promise that fulfills if this user id is valid.
|
// Returns a promise that fulfills if this user id is valid.
|
||||||
function validateAuthData(authData) {
|
async function validateAuthData(authData) {
|
||||||
return request('auth/userinfo', authData.access_token).then(data => {
|
const config = Config.get(Parse.applicationId);
|
||||||
if (data && data.sub == authData.id) {
|
|
||||||
return;
|
Deprecator.logRuntimeDeprecation({ usage: 'phantauth adapter' });
|
||||||
}
|
|
||||||
|
const phantauthConfig = config.auth.phantauth;
|
||||||
|
if (!phantauthConfig?.enableInsecureAuth) {
|
||||||
|
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'PhantAuth only works with enableInsecureAuth: true');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await request('auth/userinfo', authData.access_token);
|
||||||
|
if (data?.sub !== authData.id) {
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'PhantAuth auth is invalid for this user.');
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'PhantAuth auth is invalid for this user.');
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills if this app id is valid.
|
// Returns a promise that fulfills if this app id is valid.
|
||||||
|
|||||||
@@ -1,41 +1,112 @@
|
|||||||
// Helper functions for accessing the qq Graph API.
|
/**
|
||||||
const httpsRequest = require('./httpsRequest');
|
* Parse Server authentication adapter for QQ.
|
||||||
var Parse = require('parse/node').Parse;
|
*
|
||||||
|
* @class QqAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.clientId - Your QQ App ID. Required for secure authentication.
|
||||||
|
* @param {string} options.clientSecret - Your QQ App Secret. Required for secure authentication.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for QQ authentication, use the following structure:
|
||||||
|
* ### Secure Configuration
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "qq": {
|
||||||
|
* "clientId": "your-app-id",
|
||||||
|
* "clientSecret": "your-app-secret"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Configuration (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "qq": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **Secure Authentication**: `code`, `redirect_uri`.
|
||||||
|
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Secure Authentication Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "qq": {
|
||||||
|
* "code": "abcd1234",
|
||||||
|
* "redirect_uri": "https://your-redirect-uri.com/callback"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Authentication Payload (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "qq": {
|
||||||
|
* "id": "1234567",
|
||||||
|
* "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using QQ's OAuth API.
|
||||||
|
* - **Insecure authentication** validates the `id` and `access_token` directly, bypassing OAuth flows. This approach is not recommended and may be deprecated in future versions.
|
||||||
|
*
|
||||||
|
* @see {@link https://wiki.connect.qq.com/ QQ Authentication Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
|
||||||
function validateAuthData(authData) {
|
class QqAdapter extends BaseAuthCodeAdapter {
|
||||||
return graphRequest('me?access_token=' + authData.access_token).then(function (data) {
|
constructor() {
|
||||||
if (data && data.openid == authData.id) {
|
super('qq');
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
|
async getUserFromAccessToken(access_token) {
|
||||||
});
|
const response = await fetch('https://graph.qq.com/oauth2.0/me', {
|
||||||
}
|
headers: {
|
||||||
|
Authorization: `Bearer ${access_token}`,
|
||||||
// Returns a promise that fulfills if this app id is valid.
|
},
|
||||||
function validateAppId() {
|
});
|
||||||
return Promise.resolve();
|
|
||||||
}
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq API request failed.');
|
||||||
// A promisey wrapper for qq graph requests.
|
}
|
||||||
function graphRequest(path) {
|
|
||||||
return httpsRequest.get('https://graph.qq.com/oauth2.0/' + path, true).then(data => {
|
const data = await response.text();
|
||||||
return parseResponseData(data);
|
return this.parseResponseData(data);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
async getAccessTokenFromCode(authData) {
|
||||||
function parseResponseData(data) {
|
const response = await fetch('https://graph.qq.com/oauth2.0/token', {
|
||||||
const starPos = data.indexOf('(');
|
method: 'GET',
|
||||||
const endPos = data.indexOf(')');
|
headers: {
|
||||||
if (starPos == -1 || endPos == -1) {
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
client_id: this.clientId,
|
||||||
|
client_secret: this.clientSecret,
|
||||||
|
redirect_uri: authData.redirect_uri,
|
||||||
|
code: authData.code,
|
||||||
|
}).toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq API request failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
const data = this.parseResponseData(text);
|
||||||
|
return data.access_token;
|
||||||
}
|
}
|
||||||
data = data.substring(starPos + 1, endPos - 1);
|
|
||||||
return JSON.parse(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export default new QqAdapter();
|
||||||
validateAppId,
|
|
||||||
validateAuthData,
|
|
||||||
parseResponseData,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,44 +1,118 @@
|
|||||||
// Helper functions for accessing the Spotify API.
|
/**
|
||||||
const httpsRequest = require('./httpsRequest');
|
* Parse Server authentication adapter for Spotify.
|
||||||
var Parse = require('parse/node').Parse;
|
*
|
||||||
|
* @class SpotifyAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.clientId - Your Spotify application's Client ID. Required for secure authentication.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Spotify authentication, use the following structure:
|
||||||
|
* ### Secure Configuration
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "spotify": {
|
||||||
|
* "clientId": "your-client-id"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Configuration (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "spotify": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **Secure Authentication**: `code`, `redirect_uri`, and `code_verifier`.
|
||||||
|
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Secure Authentication Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "spotify": {
|
||||||
|
* "code": "abc123def456ghi789",
|
||||||
|
* "redirect_uri": "https://example.com/callback",
|
||||||
|
* "code_verifier": "secure-code-verifier"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Authentication Payload (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "spotify": {
|
||||||
|
* "id": "1234567",
|
||||||
|
* "access_token": "abc123def456ghi789"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - `enableInsecureAuth` is **not recommended** and bypasses secure flows by validating the user ID and access token directly. This method is not suitable for production environments and may be removed in future versions.
|
||||||
|
* - Secure authentication exchanges the `code` provided by the client for an access token using Spotify's OAuth API. This method ensures greater security and is the recommended approach.
|
||||||
|
*
|
||||||
|
* @see {@link https://developer.spotify.com/documentation/web-api/tutorials/getting-started Spotify OAuth Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
|
||||||
function validateAuthData(authData) {
|
class SpotifyAdapter extends BaseAuthCodeAdapter {
|
||||||
return request('me', authData.access_token).then(data => {
|
constructor() {
|
||||||
if (data && data.id == authData.id) {
|
super('spotify');
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
async getUserFromAccessToken(access_token) {
|
||||||
|
const response = await fetch('https://api.spotify.com/v1/me', {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + access_token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify API request failed.');
|
||||||
}
|
}
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a promise that fulfills if this app id is valid.
|
const user = await response.json();
|
||||||
async function validateAppId(appIds, authData) {
|
return {
|
||||||
const access_token = authData.access_token;
|
id: user.id,
|
||||||
if (!Array.isArray(appIds)) {
|
};
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.');
|
|
||||||
}
|
}
|
||||||
if (!appIds.length) {
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is not configured.');
|
async getAccessTokenFromCode(authData) {
|
||||||
}
|
if (!authData.code || !authData.redirect_uri || !authData.code_verifier) {
|
||||||
const data = await request('me', access_token);
|
throw new Parse.Error(
|
||||||
if (!data || !appIds.includes(data.id)) {
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.');
|
'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('https://accounts.spotify.com/api/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: authData.code,
|
||||||
|
redirect_uri: authData.redirect_uri,
|
||||||
|
code_verifier: authData.code_verifier,
|
||||||
|
client_id: this.clientId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify API request failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A promisey wrapper for Spotify API requests.
|
export default new SpotifyAdapter();
|
||||||
function request(path, access_token) {
|
|
||||||
return httpsRequest.get({
|
|
||||||
host: 'api.spotify.com',
|
|
||||||
path: '/v1/' + path,
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + access_token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAppId: validateAppId,
|
|
||||||
validateAuthData: validateAuthData,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,51 +1,244 @@
|
|||||||
// Helper functions for accessing the twitter API.
|
/**
|
||||||
var OAuth = require('./OAuth1Client');
|
* Parse Server authentication adapter for Twitter.
|
||||||
var Parse = require('parse/node').Parse;
|
*
|
||||||
|
* @class TwitterAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {string} options.consumerKey - The Twitter App Consumer Key. Required for secure authentication.
|
||||||
|
* @param {string} options.consumerSecret - The Twitter App Consumer Secret. Required for secure authentication.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Twitter authentication, use the following structure:
|
||||||
|
* ### Secure Configuration
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "twitter": {
|
||||||
|
* "consumerKey": "your-consumer-key",
|
||||||
|
* "consumerSecret": "your-consumer-secret"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Configuration (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "twitter": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **Secure Authentication**: `oauth_token`, `oauth_verifier`.
|
||||||
|
* - **Insecure Authentication (Not Recommended)**: `id`, `oauth_token`, `oauth_token_secret`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Secure Authentication Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "twitter": {
|
||||||
|
* "oauth_token": "1234567890-abc123def456",
|
||||||
|
* "oauth_verifier": "abc123def456"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Insecure Authentication Payload (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "twitter": {
|
||||||
|
* "id": "1234567890",
|
||||||
|
* "oauth_token": "1234567890-abc123def456",
|
||||||
|
* "oauth_token_secret": "1234567890-abc123def456"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - **Deprecation Notice**: `enableInsecureAuth` and insecure fields (`id`, `oauth_token_secret`) are **deprecated** and may be removed in future versions. Use secure authentication with `consumerKey` and `consumerSecret`.
|
||||||
|
* - Secure authentication exchanges the `oauth_token` and `oauth_verifier` provided by the client for an access token using Twitter's OAuth API.
|
||||||
|
*
|
||||||
|
* @see {@link https://developer.twitter.com/en/docs/authentication/oauth-1-0a Twitter OAuth Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
import Config from '../../Config';
|
||||||
function validateAuthData(authData, options) {
|
import querystring from 'querystring';
|
||||||
if (!options) {
|
import AuthAdapter from './AuthAdapter';
|
||||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Twitter auth configuration missing');
|
|
||||||
|
class TwitterAuthAdapter extends AuthAdapter {
|
||||||
|
validateOptions(options) {
|
||||||
|
if (!options) {
|
||||||
|
throw new Error('Twitter auth options are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enableInsecureAuth = options.enableInsecureAuth;
|
||||||
|
|
||||||
|
if (!this.enableInsecureAuth && (!options.consumer_key || !options.consumer_secret)) {
|
||||||
|
throw new Error('Consumer key and secret are required for secure Twitter auth.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
options = handleMultipleConfigurations(authData, options);
|
|
||||||
var client = new OAuth(options);
|
|
||||||
client.host = 'api.twitter.com';
|
|
||||||
client.auth_token = authData.auth_token;
|
|
||||||
client.auth_token_secret = authData.auth_token_secret;
|
|
||||||
|
|
||||||
return client.get('/1.1/account/verify_credentials.json').then(data => {
|
async validateAuthData(authData, options) {
|
||||||
if (data && data.id_str == '' + authData.id) {
|
const config = Config.get(Parse.applicationId);
|
||||||
|
const twitterConfig = config.auth.twitter;
|
||||||
|
|
||||||
|
if (this.enableInsecureAuth && twitterConfig && config.enableInsecureAuthAdapters) {
|
||||||
|
return this.validateInsecureAuth(authData, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.consumer_key || !options.consumer_secret) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Twitter auth configuration missing consumer_key and/or consumer_secret.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessTokenData = await this.exchangeAccessToken(authData);
|
||||||
|
|
||||||
|
if (accessTokenData?.oauth_token && accessTokenData?.user_id) {
|
||||||
|
authData.id = accessTokenData.user_id;
|
||||||
|
authData.auth_token = accessTokenData.oauth_token;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this app id is valid.
|
throw new Parse.Error(
|
||||||
function validateAppId() {
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
return Promise.resolve();
|
'Twitter auth is invalid for this user.'
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function handleMultipleConfigurations(authData, options) {
|
async validateInsecureAuth(authData, options) {
|
||||||
if (Array.isArray(options)) {
|
if (!authData.oauth_token || !authData.oauth_token_secret) {
|
||||||
const consumer_key = authData.consumer_key;
|
throw new Parse.Error(
|
||||||
if (!consumer_key) {
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
'Twitter insecure auth requires oauth_token and oauth_token_secret.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
options = options.filter(option => {
|
|
||||||
return option.consumer_key == consumer_key;
|
options = this.handleMultipleConfigurations(authData, options);
|
||||||
|
|
||||||
|
const data = await this.request(authData, options);
|
||||||
|
const parsedData = await data.json();
|
||||||
|
|
||||||
|
if (parsedData?.id === authData.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Twitter auth is invalid for this user.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exchangeAccessToken(authData) {
|
||||||
|
const accessTokenRequestOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: querystring.stringify({
|
||||||
|
oauth_token: authData.oauth_token,
|
||||||
|
oauth_verifier: authData.oauth_verifier,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch('https://api.twitter.com/oauth/access_token', accessTokenRequestOptions);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to exchange access token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMultipleConfigurations(authData, options) {
|
||||||
|
if (Array.isArray(options)) {
|
||||||
|
const consumer_key = authData.consumer_key;
|
||||||
|
|
||||||
|
if (!consumer_key) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Twitter auth is invalid for this user.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
options = options.filter(option => option.consumer_key === consumer_key);
|
||||||
|
|
||||||
|
if (options.length === 0) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Twitter auth is invalid for this user.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(authData, options) {
|
||||||
|
const { consumer_key, consumer_secret } = options;
|
||||||
|
|
||||||
|
const oauth = {
|
||||||
|
consumer_key,
|
||||||
|
consumer_secret,
|
||||||
|
auth_token: authData.oauth_token,
|
||||||
|
auth_token_secret: authData.oauth_token_secret,
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = new URL('https://api.twitter.com/2/users/me');
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + oauth.auth_token,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(oauth),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.length == 0) {
|
if (!response.ok) {
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
throw new Error('Failed to fetch user data.');
|
||||||
}
|
}
|
||||||
options = options[0];
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeFind(authData) {
|
||||||
|
if (this.enableInsecureAuth && !authData?.code) {
|
||||||
|
if (!authData?.access_token) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.getUserFromAccessToken(authData.access_token, authData);
|
||||||
|
|
||||||
|
if (user.id !== authData.id) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authData?.code) {
|
||||||
|
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Twitter code is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const access_token = await this.exchangeAccessToken(authData);
|
||||||
|
const user = await this.getUserFromAccessToken(access_token, authData);
|
||||||
|
|
||||||
|
|
||||||
|
authData.access_token = access_token;
|
||||||
|
authData.id = user.id;
|
||||||
|
|
||||||
|
delete authData.code;
|
||||||
|
delete authData.redirect_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAppId() {
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export default new TwitterAuthAdapter();
|
||||||
validateAppId,
|
|
||||||
validateAuthData,
|
|
||||||
handleMultipleConfigurations,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,28 +4,32 @@
|
|||||||
|
|
||||||
const httpsRequest = require('./httpsRequest');
|
const httpsRequest = require('./httpsRequest');
|
||||||
var Parse = require('parse/node').Parse;
|
var Parse = require('parse/node').Parse;
|
||||||
|
import Config from '../../Config';
|
||||||
|
import Deprecator from '../../Deprecator/Deprecator';
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
function validateAuthData(authData, params) {
|
async function validateAuthData(authData, params) {
|
||||||
return vkOAuth2Request(params).then(function (response) {
|
const config = Config.get(Parse.applicationId);
|
||||||
if (response && response.access_token) {
|
Deprecator.logRuntimeDeprecation({ usage: 'vkontakte adapter' });
|
||||||
return request(
|
|
||||||
'api.vk.com',
|
const vkConfig = config.auth.vkontakte;
|
||||||
'method/users.get?access_token=' + authData.access_token + '&v=' + params.apiVersion
|
if (!vkConfig?.enableInsecureAuth || !config.enableInsecureAuthAdapters) {
|
||||||
).then(function (response) {
|
throw new Parse.Error('Vk only works with enableInsecureAuth: true');
|
||||||
if (
|
}
|
||||||
response &&
|
|
||||||
response.response &&
|
const response = await vkOAuth2Request(params);
|
||||||
response.response.length &&
|
if (!response?.access_token) {
|
||||||
response.response[0].id == authData.id
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk appIds or appSecret is incorrect.');
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk appIds or appSecret is incorrect.');
|
||||||
});
|
}
|
||||||
|
|
||||||
|
const vkUser = await request(
|
||||||
|
'api.vk.com',
|
||||||
|
`method/users.get?access_token=${authData.access_token}&v=${params.apiVersion}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!vkUser?.response?.length || vkUser.response[0].id !== authData.id) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function vkOAuth2Request(params) {
|
function vkOAuth2Request(params) {
|
||||||
|
|||||||
@@ -1,30 +1,120 @@
|
|||||||
// Helper functions for accessing the WeChat Graph API.
|
/**
|
||||||
const httpsRequest = require('./httpsRequest');
|
* Parse Server authentication adapter for WeChat.
|
||||||
var Parse = require('parse/node').Parse;
|
*
|
||||||
|
* @class WeChatAdapter
|
||||||
|
* @param {Object} options - The adapter options object.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
* @param {string} options.clientId - Your WeChat App ID.
|
||||||
|
* @param {string} options.clientSecret - Your WeChat App Secret.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for WeChat authentication, use the following structure:
|
||||||
|
* ### Secure Configuration (Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "wechat": {
|
||||||
|
* "clientId": "your-client-id",
|
||||||
|
* "clientSecret": "your-client-secret"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Configuration (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "wechat": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **With `enableInsecureAuth` (Not Recommended)**: `id`, `access_token`.
|
||||||
|
* - **Without `enableInsecureAuth`**: `code`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Secure Authentication Payload (Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "wechat": {
|
||||||
|
* "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Authentication Payload (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "wechat": {
|
||||||
|
* "id": "1234567",
|
||||||
|
* "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - With `enableInsecureAuth`, the adapter directly validates the `id` and `access_token` sent by the client.
|
||||||
|
* - Without `enableInsecureAuth`, the adapter uses the `code` provided by the client to exchange for an access token via WeChat's OAuth API.
|
||||||
|
* - The `enableInsecureAuth` flag is **deprecated** and may be removed in future versions. Use secure authentication with the `code` field instead.
|
||||||
|
*
|
||||||
|
* @example <caption>Auth Data Example</caption>
|
||||||
|
* // Example authData provided by the client:
|
||||||
|
* const authData = {
|
||||||
|
* wechat: {
|
||||||
|
* code: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @see {@link https://developers.weixin.qq.com/doc/offiaccount/en/OA_Web_Apps/Wechat_webpage_authorization.html WeChat Authentication Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
|
||||||
function validateAuthData(authData) {
|
|
||||||
return graphRequest('auth?access_token=' + authData.access_token + '&openid=' + authData.id).then(
|
class WeChatAdapter extends BaseAuthCodeAdapter {
|
||||||
function (data) {
|
constructor() {
|
||||||
if (data.errcode == 0) {
|
super('WeChat');
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'wechat auth is invalid for this user.');
|
async getUserFromAccessToken(access_token, authData) {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.weixin.qq.com/sns/auth?access_token=${access_token}&openid=${authData.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || data.errcode !== 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth is invalid for this user.');
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessTokenFromCode(authData) {
|
||||||
|
if (!authData.code) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth requires a code to be sent.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const appId = this.clientId;
|
||||||
|
const appSecret = this.clientSecret
|
||||||
|
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${authData.code}&grant_type=authorization_code`
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || data.errcode) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth is invalid for this user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
authData.id = data.openid;
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills if this app id is valid.
|
export default new WeChatAdapter();
|
||||||
function validateAppId() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A promisey wrapper for WeChat graph requests.
|
|
||||||
function graphRequest(path) {
|
|
||||||
return httpsRequest.get('https://api.weixin.qq.com/sns/' + path);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAppId,
|
|
||||||
validateAuthData,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,41 +1,149 @@
|
|||||||
// Helper functions for accessing the weibo Graph API.
|
/**
|
||||||
var httpsRequest = require('./httpsRequest');
|
* Parse Server authentication adapter for Weibo.
|
||||||
var Parse = require('parse/node').Parse;
|
*
|
||||||
var querystring = require('querystring');
|
* @class WeiboAdapter
|
||||||
|
* @param {Object} options - The adapter configuration options.
|
||||||
|
* @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
|
||||||
|
* @param {string} options.clientId - Your Weibo client ID.
|
||||||
|
* @param {string} options.clientSecret - Your Weibo client secret.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* ## Parse Server Configuration
|
||||||
|
* To configure Parse Server for Weibo authentication, use the following structure:
|
||||||
|
* ### Secure Configuration
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "weibo": {
|
||||||
|
* "clientId": "your-client-id",
|
||||||
|
* "clientSecret": "your-client-secret"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Configuration (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "auth": {
|
||||||
|
* "weibo": {
|
||||||
|
* "enableInsecureAuth": true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The adapter requires the following `authData` fields:
|
||||||
|
* - **Secure Authentication**: `code`, `redirect_uri`.
|
||||||
|
* - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
|
||||||
|
*
|
||||||
|
* ## Auth Payloads
|
||||||
|
* ### Secure Authentication Payload
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "weibo": {
|
||||||
|
* "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
* "redirect_uri": "https://example.com/callback"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* ### Insecure Authentication Payload (Not Recommended)
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "weibo": {
|
||||||
|
* "id": "1234567",
|
||||||
|
* "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Notes
|
||||||
|
* - **Insecure Authentication**: When `enableInsecureAuth` is enabled, the adapter directly validates the `id` and `access_token` provided by the client.
|
||||||
|
* - **Secure Authentication**: When `enableInsecureAuth` is disabled, the adapter exchanges the `code` and `redirect_uri` for an access token using Weibo's OAuth API.
|
||||||
|
* - `enableInsecureAuth` is **deprecated** and may be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
|
||||||
|
*
|
||||||
|
* @example <caption>Auth Data Example (Secure)</caption>
|
||||||
|
* const authData = {
|
||||||
|
* weibo: {
|
||||||
|
* code: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
* redirect_uri: "https://example.com/callback"
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @example <caption>Auth Data Example (Insecure - Not Recommended)</caption>
|
||||||
|
* const authData = {
|
||||||
|
* weibo: {
|
||||||
|
* id: "1234567",
|
||||||
|
* access_token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @see {@link https://open.weibo.com/wiki/Oauth2/access_token Weibo Authentication Documentation}
|
||||||
|
*/
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
|
||||||
function validateAuthData(authData) {
|
import querystring from 'querystring';
|
||||||
return graphRequest(authData.access_token).then(function (data) {
|
|
||||||
if (data && data.uid == authData.id) {
|
class WeiboAdapter extends BaseAuthCodeAdapter {
|
||||||
return;
|
constructor() {
|
||||||
|
super('Weibo');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserFromAccessToken(access_token) {
|
||||||
|
const postData = querystring.stringify({
|
||||||
|
access_token: access_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch('https://api.weibo.com/oauth2/get_token_info', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: postData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Weibo auth is invalid for this user.');
|
||||||
}
|
}
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'weibo auth is invalid for this user.');
|
|
||||||
});
|
return {
|
||||||
|
id: data.uid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessTokenFromCode(authData) {
|
||||||
|
if (!authData?.code || !authData?.redirect_uri) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Weibo auth requires code and redirect_uri to be sent.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const postData = querystring.stringify({
|
||||||
|
client_id: this.clientId,
|
||||||
|
client_secret: this.clientSecret,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: authData.code,
|
||||||
|
redirect_uri: authData.redirect_uri,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch('https://api.weibo.com/oauth2/access_token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: postData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || data.errcode) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Weibo auth is invalid for this user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills if this app id is valid.
|
export default new WeiboAdapter();
|
||||||
function validateAppId() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A promisey wrapper for weibo graph requests.
|
|
||||||
function graphRequest(access_token) {
|
|
||||||
var postData = querystring.stringify({
|
|
||||||
access_token: access_token,
|
|
||||||
});
|
|
||||||
var options = {
|
|
||||||
hostname: 'api.weibo.com',
|
|
||||||
path: '/oauth2/get_token_info',
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Content-Length': Buffer.byteLength(postData),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return httpsRequest.request(options, postData);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateAppId,
|
|
||||||
validateAuthData,
|
|
||||||
};
|
|
||||||
|
|||||||
47
src/Auth.js
47
src/Auth.js
@@ -417,26 +417,35 @@ Auth.prototype._getAllRolesNamesForRoleIds = function (roleIDs, names = [], quer
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const findUsersWithAuthData = (config, authData) => {
|
const findUsersWithAuthData = async (config, authData, beforeFind) => {
|
||||||
const providers = Object.keys(authData);
|
const providers = Object.keys(authData);
|
||||||
const query = providers
|
|
||||||
.reduce((memo, provider) => {
|
|
||||||
if (!authData[provider] || (authData && !authData[provider].id)) {
|
|
||||||
return memo;
|
|
||||||
}
|
|
||||||
const queryKey = `authData.${provider}.id`;
|
|
||||||
const query = {};
|
|
||||||
query[queryKey] = authData[provider].id;
|
|
||||||
memo.push(query);
|
|
||||||
return memo;
|
|
||||||
}, [])
|
|
||||||
.filter(q => {
|
|
||||||
return typeof q !== 'undefined';
|
|
||||||
});
|
|
||||||
|
|
||||||
return query.length > 0
|
const queries = await Promise.all(
|
||||||
? config.database.find('_User', { $or: query }, { limit: 2 })
|
providers.map(async provider => {
|
||||||
: Promise.resolve([]);
|
const providerAuthData = authData[provider];
|
||||||
|
|
||||||
|
const adapter = config.authDataManager.getValidatorForProvider(provider)?.adapter;
|
||||||
|
if (beforeFind && typeof adapter?.beforeFind === 'function') {
|
||||||
|
await adapter.beforeFind(providerAuthData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!providerAuthData?.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { [`authData.${provider}.id`]: providerAuthData.id };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter out null queries
|
||||||
|
const validQueries = queries.filter(query => query !== null);
|
||||||
|
|
||||||
|
if (!validQueries.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform database query
|
||||||
|
return config.database.find('_User', { $or: validQueries }, { limit: 2 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasMutatedAuthData = (authData, userAuthData) => {
|
const hasMutatedAuthData = (authData, userAuthData) => {
|
||||||
@@ -539,7 +548,7 @@ const handleAuthDataValidation = async (authData, req, foundUser) => {
|
|||||||
acc.authData[provider] = null;
|
acc.authData[provider] = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const { validator } = req.config.authDataManager.getValidatorForProvider(provider);
|
const { validator } = req.config.authDataManager.getValidatorForProvider(provider) || {};
|
||||||
const authProvider = (req.config.auth || {})[provider] || {};
|
const authProvider = (req.config.auth || {})[provider] || {};
|
||||||
if (!validator || authProvider.enabled === false) {
|
if (!validator || authProvider.enabled === false) {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
SecurityOptions,
|
SecurityOptions,
|
||||||
} from './Options/Definitions';
|
} from './Options/Definitions';
|
||||||
import ParseServer from './cloud-code/Parse.Server';
|
import ParseServer from './cloud-code/Parse.Server';
|
||||||
|
import Deprecator from './Deprecator/Deprecator';
|
||||||
|
|
||||||
function removeTrailingSlash(str) {
|
function removeTrailingSlash(str) {
|
||||||
if (!str) {
|
if (!str) {
|
||||||
@@ -84,6 +85,7 @@ export class Config {
|
|||||||
pages,
|
pages,
|
||||||
security,
|
security,
|
||||||
enforcePrivateUsers,
|
enforcePrivateUsers,
|
||||||
|
enableInsecureAuthAdapters,
|
||||||
schema,
|
schema,
|
||||||
requestKeywordDenylist,
|
requestKeywordDenylist,
|
||||||
allowExpiredAuthDataToken,
|
allowExpiredAuthDataToken,
|
||||||
@@ -129,6 +131,7 @@ export class Config {
|
|||||||
this.validateSecurityOptions(security);
|
this.validateSecurityOptions(security);
|
||||||
this.validateSchemaOptions(schema);
|
this.validateSchemaOptions(schema);
|
||||||
this.validateEnforcePrivateUsers(enforcePrivateUsers);
|
this.validateEnforcePrivateUsers(enforcePrivateUsers);
|
||||||
|
this.validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters);
|
||||||
this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken);
|
this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken);
|
||||||
this.validateRequestKeywordDenylist(requestKeywordDenylist);
|
this.validateRequestKeywordDenylist(requestKeywordDenylist);
|
||||||
this.validateRateLimit(rateLimit);
|
this.validateRateLimit(rateLimit);
|
||||||
@@ -504,6 +507,15 @@ export class Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters) {
|
||||||
|
if (enableInsecureAuthAdapters && typeof enableInsecureAuthAdapters !== 'boolean') {
|
||||||
|
throw 'Parse Server option enableInsecureAuthAdapters must be a boolean.';
|
||||||
|
}
|
||||||
|
if (enableInsecureAuthAdapters) {
|
||||||
|
Deprecator.logRuntimeDeprecation({ usage: 'insecure adapter' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get mount() {
|
get mount() {
|
||||||
var mount = this._mount;
|
var mount = this._mount;
|
||||||
if (this.publicServerURL) {
|
if (this.publicServerURL) {
|
||||||
|
|||||||
@@ -15,4 +15,7 @@
|
|||||||
*
|
*
|
||||||
* If there are no deprecations, this must return an empty array.
|
* If there are no deprecations, this must return an empty array.
|
||||||
*/
|
*/
|
||||||
module.exports = [{ optionKey: 'encodeParseObjectInCloudFunction', changeNewKey: '' }]
|
module.exports = [
|
||||||
|
{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
|
||||||
|
{ optionKey: 'enableInsecureAuthAdapters', changeNewDefault: 'false' },
|
||||||
|
];
|
||||||
|
|||||||
@@ -233,6 +233,13 @@ module.exports.ParseServerOptions = {
|
|||||||
action: parsers.booleanParser,
|
action: parsers.booleanParser,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
enableInsecureAuthAdapters: {
|
||||||
|
env: 'PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS',
|
||||||
|
help:
|
||||||
|
'Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.',
|
||||||
|
action: parsers.booleanParser,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
encodeParseObjectInCloudFunction: {
|
encodeParseObjectInCloudFunction: {
|
||||||
env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
|
env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
|
||||||
help:
|
help:
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
* @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true
|
* @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true
|
||||||
* @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.
|
* @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.
|
||||||
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
|
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
|
||||||
|
* @property {Boolean} enableInsecureAuthAdapters Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
|
||||||
* @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br>ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
|
* @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br>ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
|
||||||
* @property {String} encryptionKey Key for encrypting your files
|
* @property {String} encryptionKey Key for encrypting your files
|
||||||
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
|
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
|
||||||
|
|||||||
@@ -161,6 +161,10 @@ export interface ParseServerOptions {
|
|||||||
/* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
|
/* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
|
||||||
:ENV: PARSE_SERVER_AUTH_PROVIDERS */
|
:ENV: PARSE_SERVER_AUTH_PROVIDERS */
|
||||||
auth: ?{ [string]: AuthAdapter };
|
auth: ?{ [string]: AuthAdapter };
|
||||||
|
/* Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
|
||||||
|
:ENV: PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS
|
||||||
|
:DEFAULT: true */
|
||||||
|
enableInsecureAuthAdapters: ?boolean;
|
||||||
/* Max file size for uploads, defaults to 20mb
|
/* Max file size for uploads, defaults to 20mb
|
||||||
:DEFAULT: 20mb */
|
:DEFAULT: 20mb */
|
||||||
maxUploadSize: ?string;
|
maxUploadSize: ?string;
|
||||||
|
|||||||
@@ -458,9 +458,8 @@ RestWrite.prototype.validateAuthData = function () {
|
|||||||
var providers = Object.keys(authData);
|
var providers = Object.keys(authData);
|
||||||
if (providers.length > 0) {
|
if (providers.length > 0) {
|
||||||
const canHandleAuthData = providers.some(provider => {
|
const canHandleAuthData = providers.some(provider => {
|
||||||
var providerAuthData = authData[provider];
|
const providerAuthData = authData[provider] || {};
|
||||||
var hasToken = providerAuthData && providerAuthData.id;
|
return !!Object.keys(providerAuthData).length;
|
||||||
return hasToken || providerAuthData === null;
|
|
||||||
});
|
});
|
||||||
if (canHandleAuthData || hasUsernameAndPassword || this.auth.isMaster || this.getUserId()) {
|
if (canHandleAuthData || hasUsernameAndPassword || this.auth.isMaster || this.getUserId()) {
|
||||||
return this.handleAuthData(authData);
|
return this.handleAuthData(authData);
|
||||||
@@ -520,7 +519,7 @@ RestWrite.prototype.ensureUniqueAuthDataId = async function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
RestWrite.prototype.handleAuthData = async function (authData) {
|
RestWrite.prototype.handleAuthData = async function (authData) {
|
||||||
const r = await Auth.findUsersWithAuthData(this.config, authData);
|
const r = await Auth.findUsersWithAuthData(this.config, authData, true);
|
||||||
const results = this.filteredObjectsByACL(r);
|
const results = this.filteredObjectsByACL(r);
|
||||||
|
|
||||||
const userId = this.getUserId();
|
const userId = this.getUserId();
|
||||||
|
|||||||
@@ -69,6 +69,17 @@ class CheckGroupServerConfig extends CheckGroup {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
new Check({
|
||||||
|
title: 'Insecure auth adapters disabled',
|
||||||
|
warning:
|
||||||
|
"Attackers may explore insecure auth adapters' vulnerabilities and log in on behalf of another user.",
|
||||||
|
solution: "Change Parse Server configuration to 'enableInsecureAuthAdapters: false'.",
|
||||||
|
check: () => {
|
||||||
|
if (config.enableInsecureAuthAdapters !== false) {
|
||||||
|
throw 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ runner({
|
|||||||
help,
|
help,
|
||||||
usage: '[options] <path/to/configuration.json>',
|
usage: '[options] <path/to/configuration.json>',
|
||||||
start: function (program, options, logOptions) {
|
start: function (program, options, logOptions) {
|
||||||
|
|
||||||
if (!options.appId || !options.masterKey) {
|
if (!options.appId || !options.masterKey) {
|
||||||
program.outputHelp();
|
program.outputHelp();
|
||||||
console.error('');
|
console.error('');
|
||||||
|
|||||||
Reference in New Issue
Block a user