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:
Manuel
2025-03-21 10:49:09 +01:00
committed by GitHub
parent c56b2c49b2
commit 5ef0440c8e
59 changed files with 5987 additions and 1680 deletions

View 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' });
});
});
});

View 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();
});
});
});

View 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();
});
});
});

View 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();
});
});
});

View 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.'
);
});
});
});

View 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.'
);
});
});
});

View 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)
);
});
});
});

View 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.'
);
});
});
});

View 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.')
);
});
});
});

View 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();
});
});
});

View 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.'
);
});
});
});

View 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.'
);
});
});
});

View 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.' })
);
});
});
});

View 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.' })
);
});
});
});

View File

@@ -3,99 +3,8 @@ const Config = require('../lib/Config');
const defaultColumns = require('../lib/Controllers/SchemaController').defaultColumns;
const authenticationLoader = require('../lib/Adapters/Auth');
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 () {
[
'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 () {
return {
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', () => {
const google = require('../lib/Adapters/Auth/google');
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', () => {
const keycloak = require('../lib/Adapters/Auth/keycloak');
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', () => {
const apple = require('../lib/Adapters/Auth/apple');
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', () => {
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
it('validateAuthData should throw for invalid auth', async () => {
await reconfigureServer({
auth: {
phantauth: {
enableInsecureAuth: true,
}
}
})
const authData = {
id: 'fakeid',
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', () => {
const facebook = require('../lib/Adapters/Auth/facebook');
const jwt = require('jsonwebtoken');

View File

@@ -355,16 +355,16 @@ describe('Auth Adapter features', () => {
const authData = user.get('authData').modernAdapter3;
expect(authData).toEqual({ foo: 'bar' });
for (const call of afterSpy.calls.all()) {
const args = call.args[0];
const args = call.args[2];
if (args.user) {
user._objCount = args.user._objCount;
break;
}
}
expect(afterSpy).toHaveBeenCalledWith(
{ ip: '127.0.0.1', user, master: false },
{ id: 'modernAdapter3Data' },
undefined
undefined,
{ ip: '127.0.0.1', user, master: false },
);
expect(spy).toHaveBeenCalled();
});

View File

@@ -32,6 +32,7 @@ describe('Security Check Groups', () => {
config.masterKey = 'aMoreSecur3Passwor7!';
config.security.enableCheckLog = false;
config.allowClientClassCreation = false;
config.enableInsecureAuthAdapters = false;
await reconfigureServer(config);
const group = new CheckGroupServerConfig();
@@ -39,6 +40,7 @@ describe('Security Check Groups', () => {
expect(group.checks()[0].checkState()).toBe(CheckState.success);
expect(group.checks()[1].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 () => {
@@ -52,6 +54,7 @@ describe('Security Check Groups', () => {
expect(group.checks()[0].checkState()).toBe(CheckState.fail);
expect(group.checks()[1].checkState()).toBe(CheckState.fail);
expect(group.checks()[2].checkState()).toBe(CheckState.fail);
expect(group.checks()[4].checkState()).toBe(CheckState.fail);
});
});

View File

@@ -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();
}
});
});

View File

@@ -9,6 +9,7 @@ module.exports = [
globals: {
...globals.node,
...globals.jasmine,
mockFetch: "readonly",
Parse: "readonly",
reconfigureServer: "readonly",
createTestUser: "readonly",

View File

@@ -414,6 +414,25 @@ function mockShortLivedAuth() {
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.
global.Parse = Parse;
global.TestObject = TestObject;
@@ -429,6 +448,7 @@ global.arrayContains = arrayContains;
global.jequal = jequal;
global.range = range;
global.reconfigureServer = reconfigureServer;
global.mockFetch = mockFetch;
global.defaultConfiguration = defaultConfiguration;
global.mockCustomAuthenticator = mockCustomAuthenticator;
global.mockFacebookAuthenticator = mockFacebookAuthenticator;

View File

@@ -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-----

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{
"spec_dir": "spec",
"spec_files": ["*spec.js"],
"spec_files": ["**/*.[sS]pec.js"],
"helpers": ["helper.js"],
"random": true
}