Files
kami-parse-server/spec/AuthenticationAdapters.spec.js
Maravilho Singa b60502d8a0 Instagram: Support passing in API url (#6398)
* Update instagram.js

Instagram API was updated.

* Update instagram.js

Instagram API was updated and is not allowing anymore to setup new projects to use the old style but it is still working for the ones that have it already setup.

New docs are listed here: https://developers.facebook.com/docs/instagram-basic-display-api/

I've added support for both old and new API
To use new API just add new field "api_type" = "new_api" in client side.

For old API just no changes needed.

* support api url

Co-authored-by: Diamond Lewis <findlewis@gmail.com>
2020-03-22 11:24:22 -05:00

1611 lines
47 KiB
JavaScript

const request = require('../lib/request');
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: { data: { 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',
'facebookaccountkit',
'github',
'instagram',
'google',
'linkedin',
'meetup',
'twitter',
'janrainengage',
'janraincapture',
'line',
'vkontakte',
'qq',
'spotify',
'wechat',
'weibo',
'phantauth',
'microsoft',
].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'];
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.59&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);
});
});
const getMockMyOauthProvider = function() {
return {
authData: {
id: '12345',
access_token: '12345',
expiration_date: new Date().toJSON(),
},
shouldError: false,
loggedOut: false,
synchronizedUserId: null,
synchronizedAuthToken: null,
synchronizedExpiration: null,
authenticate: function(options) {
if (this.shouldError) {
options.error(this, 'An error occurred');
} else if (this.shouldCancel) {
options.error(this, null);
} else {
options.success(this, this.authData);
}
},
restoreAuthentication: function(authData) {
if (!authData) {
this.synchronizedUserId = null;
this.synchronizedAuthToken = null;
this.synchronizedExpiration = null;
return true;
}
this.synchronizedUserId = authData.id;
this.synchronizedAuthToken = authData.access_token;
this.synchronizedExpiration = authData.expiration_date;
return true;
},
getAuthType: function() {
return 'myoauth';
},
deauthenticate: function() {
this.loggedOut = true;
this.restoreAuthentication(null);
},
};
};
Parse.User.extend({
extended: function() {
return true;
},
});
const createOAuthUser = function(callback) {
return createOAuthUserWithSessionToken(undefined, callback);
};
const createOAuthUserWithSessionToken = function(token, callback) {
const jsonBody = {
authData: {
myoauth: getMockMyOauthProvider().authData,
},
};
const options = {
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo',
'X-Parse-Session-Token': token,
'Content-Type': 'application/json',
},
url: 'http://localhost:8378/1/users',
body: jsonBody,
};
return request(options)
.then(response => {
if (callback) {
callback(null, response, response.data);
}
return {
res: response,
body: response.data,
};
})
.catch(error => {
if (callback) {
callback(error);
}
throw error;
});
};
it('should create user with REST API', done => {
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
const b = body;
ok(b.sessionToken);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
const sessionToken = b.sessionToken;
const q = new Parse.Query('_Session');
q.equalTo('sessionToken', sessionToken);
q.first({ useMasterKey: true })
.then(res => {
if (!res) {
fail('should not fail fetching the session');
done();
return;
}
expect(res.get('installationId')).toEqual('yolo');
done();
})
.catch(() => {
fail('should not fail fetching the session');
done();
});
});
});
it('should only create a single user with REST API', done => {
let objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
const b = body;
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
objectId = b.objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
const b = body;
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
expect(b.objectId).toBe(objectId);
done();
});
});
});
it("should fail to link if session token don't match user", done => {
Parse.User.signUp('myUser', 'password')
.then(user => {
return createOAuthUserWithSessionToken(user.getSessionToken());
})
.then(() => {
return Parse.User.logOut();
})
.then(() => {
return Parse.User.signUp('myUser2', 'password');
})
.then(user => {
return createOAuthUserWithSessionToken(user.getSessionToken());
})
.then(fail, ({ data }) => {
expect(data.code).toBe(208);
expect(data.error).toBe('this auth is already used');
done();
})
.catch(done.fail);
});
it('unlink and link with custom provider', async () => {
const provider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
const model = await Parse.User._logInWith('myoauth');
ok(model instanceof Parse.User, 'Model should be a Parse.User');
strictEqual(Parse.User.current(), model);
ok(model.extended(), 'Should have used the subclass.');
strictEqual(provider.authData.id, provider.synchronizedUserId);
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
strictEqual(
provider.authData.expiration_date,
provider.synchronizedExpiration
);
ok(model._isLinked('myoauth'), 'User should be linked to myoauth');
await model._unlinkFrom('myoauth');
ok(!model._isLinked('myoauth'), 'User should not be linked to myoauth');
ok(!provider.synchronizedUserId, 'User id should be cleared');
ok(!provider.synchronizedAuthToken, 'Auth token should be cleared');
ok(!provider.synchronizedExpiration, 'Expiration should be cleared');
// make sure the auth data is properly deleted
const config = Config.get(Parse.applicationId);
const res = await config.database.adapter.find(
'_User',
{
fields: Object.assign(
{},
defaultColumns._Default,
defaultColumns._Installation
),
},
{ objectId: model.id },
{}
);
expect(res.length).toBe(1);
expect(res[0]._auth_data_myoauth).toBeUndefined();
expect(res[0]._auth_data_myoauth).not.toBeNull();
await model._linkWith('myoauth');
ok(provider.synchronizedUserId, 'User id should have a value');
ok(provider.synchronizedAuthToken, 'Auth token should have a value');
ok(provider.synchronizedExpiration, 'Expiration should have a value');
ok(model._isLinked('myoauth'), 'User should be linked to myoauth');
});
function validateValidator(validator) {
expect(typeof validator).toBe('function');
}
function validateAuthenticationHandler(authenticationHandler) {
expect(authenticationHandler).not.toBeUndefined();
expect(typeof authenticationHandler.getValidatorForProvider).toBe(
'function'
);
expect(typeof authenticationHandler.getValidatorForProvider).toBe(
'function'
);
}
function validateAuthenticationAdapter(authAdapter) {
expect(authAdapter).not.toBeUndefined();
if (!authAdapter) {
return;
}
expect(typeof authAdapter.validateAuthData).toBe('function');
expect(typeof authAdapter.validateAppId).toBe('function');
}
it('properly loads custom adapter', done => {
const validAuthData = {
id: 'hello',
token: 'world',
};
const adapter = {
validateAppId: function() {
return Promise.resolve();
},
validateAuthData: function(authData) {
if (
authData.id == validAuthData.id &&
authData.token == validAuthData.token
) {
return Promise.resolve();
}
return Promise.reject();
},
};
const authDataSpy = spyOn(adapter, 'validateAuthData').and.callThrough();
const appIdSpy = spyOn(adapter, 'validateAppId').and.callThrough();
const authenticationHandler = authenticationLoader({
customAuthentication: adapter,
});
validateAuthenticationHandler(authenticationHandler);
const validator = authenticationHandler.getValidatorForProvider(
'customAuthentication'
);
validateValidator(validator);
validator(validAuthData).then(
() => {
expect(authDataSpy).toHaveBeenCalled();
// AppIds are not provided in the adapter, should not be called
expect(appIdSpy).not.toHaveBeenCalled();
done();
},
err => {
jfail(err);
done();
}
);
});
it('properly loads custom adapter module object', done => {
const authenticationHandler = authenticationLoader({
customAuthentication: path.resolve('./spec/support/CustomAuth.js'),
});
validateAuthenticationHandler(authenticationHandler);
const validator = authenticationHandler.getValidatorForProvider(
'customAuthentication'
);
validateValidator(validator);
validator({
token: 'my-token',
}).then(
() => {
done();
},
err => {
jfail(err);
done();
}
);
});
it('properly loads custom adapter module object (again)', done => {
const authenticationHandler = authenticationLoader({
customAuthentication: {
module: path.resolve('./spec/support/CustomAuthFunction.js'),
options: { token: 'valid-token' },
},
});
validateAuthenticationHandler(authenticationHandler);
const validator = authenticationHandler.getValidatorForProvider(
'customAuthentication'
);
validateValidator(validator);
validator({
token: 'valid-token',
}).then(
() => {
done();
},
err => {
jfail(err);
done();
}
);
});
it('properly loads a default adapter with options', () => {
const options = {
facebook: {
appIds: ['a', 'b'],
appSecret: 'secret',
},
};
const {
adapter,
appIds,
providerOptions,
} = authenticationLoader.loadAuthAdapter('facebook', options);
validateAuthenticationAdapter(adapter);
expect(appIds).toEqual(['a', 'b']);
expect(providerOptions).toEqual(options.facebook);
});
it('should handle Facebook appSecret for validating appIds', async () => {
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve({ id: 'a' });
});
const options = {
facebook: {
appIds: ['a', 'b'],
appSecret: 'secret_sauce',
},
};
const authData = {
access_token: 'badtoken',
};
const {
adapter,
appIds,
providerOptions,
} = authenticationLoader.loadAuthAdapter('facebook', options);
await adapter.validateAppId(appIds, authData, providerOptions);
expect(
httpsRequest.get.calls.first().args[0].includes('appsecret_proof')
).toBe(true);
});
it('should handle Facebook appSecret for validating auth data', async () => {
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve();
});
const options = {
facebook: {
appIds: ['a', 'b'],
appSecret: 'secret_sauce',
},
};
const authData = {
id: 'test',
access_token: 'test',
};
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
'facebook',
options
);
await adapter.validateAuthData(authData, providerOptions);
expect(
httpsRequest.get.calls.first().args[0].includes('appsecret_proof')
).toBe(true);
});
it('properly loads a custom adapter with options', () => {
const options = {
custom: {
validateAppId: () => {},
validateAuthData: () => {},
appIds: ['a', 'b'],
},
};
const {
adapter,
appIds,
providerOptions,
} = authenticationLoader.loadAuthAdapter('custom', options);
validateAuthenticationAdapter(adapter);
expect(appIds).toEqual(['a', 'b']);
expect(providerOptions).toEqual(options.custom);
});
it('properly loads Facebook accountkit adapter with options', () => {
const options = {
facebookaccountkit: {
appIds: ['a', 'b'],
appSecret: 'secret',
},
};
const {
adapter,
appIds,
providerOptions,
} = authenticationLoader.loadAuthAdapter('facebookaccountkit', options);
validateAuthenticationAdapter(adapter);
expect(appIds).toEqual(['a', 'b']);
expect(providerOptions.appSecret).toEqual('secret');
});
it('should fail if Facebook appIds is not configured properly', done => {
const options = {
facebookaccountkit: {
appIds: [],
},
};
const { adapter, appIds } = authenticationLoader.loadAuthAdapter(
'facebookaccountkit',
options
);
adapter.validateAppId(appIds).then(done.fail, err => {
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
done();
});
});
it('should fail to validate Facebook accountkit auth with bad token', done => {
const options = {
facebookaccountkit: {
appIds: ['a', 'b'],
},
};
const authData = {
id: 'fakeid',
access_token: 'badtoken',
};
const { adapter } = authenticationLoader.loadAuthAdapter(
'facebookaccountkit',
options
);
adapter.validateAuthData(authData).then(done.fail, err => {
expect(err.code).toBe(190);
expect(err.type).toBe('OAuthException');
done();
});
});
it('should fail to validate Facebook accountkit auth with bad token regardless of app secret proof', done => {
const options = {
facebookaccountkit: {
appIds: ['a', 'b'],
appSecret: 'badsecret',
},
};
const authData = {
id: 'fakeid',
access_token: 'badtoken',
};
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
'facebookaccountkit',
options
);
adapter.validateAuthData(authData, providerOptions).then(done.fail, err => {
expect(err.code).toBe(190);
expect(err.type).toBe('OAuthException');
done();
});
});
});
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://api.instagram.com/v1/users/self/?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/users/self/?access_token=the_token'
);
});
});
describe('google auth adapter', () => {
const google = require('../lib/Adapters/Auth/google');
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
it('should use id_token for validation is passed', async () => {
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve({ sub: 'userId' });
});
await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {});
});
it('should use id_token for validation is passed and responds with user_id', async () => {
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve({ user_id: 'userId' });
});
await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {});
});
it('should use access_token for validation is passed and responds with user_id', async () => {
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve({ user_id: 'userId' });
});
await google.validateAuthData(
{ id: 'userId', access_token: 'the_token' },
{}
);
});
it('should use access_token for validation is passed with sub', async () => {
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve({ sub: 'userId' });
});
await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {});
});
it('should fail when the id_token is invalid', async () => {
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve({ sub: 'badId' });
});
try {
await google.validateAuthData(
{ id: 'userId', id_token: 'the_token' },
{}
);
fail();
} catch (e) {
expect(e.message).toBe('Google auth is invalid for this user.');
}
});
it('should fail when the access_token is invalid', async () => {
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve({ sub: 'badId' });
});
try {
await google.validateAuthData(
{ id: 'userId', access_token: 'the_token' },
{}
);
fail();
} catch (e) {
expect(e.message).toBe('Google auth is invalid for this user.');
}
});
});
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('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');
const util = require('util');
it('(using client id as string) should throw error with missing id_token', async () => {
try {
await apple.validateAuthData({}, { clientId: 'secret' });
fail();
} catch (e) {
expect(e.message).toBe('id token is invalid for this user.');
}
});
it('(using client id as array) should throw error with missing id_token', async () => {
try {
await apple.validateAuthData({}, { client_id: ['secret'] });
fail();
} catch (e) {
expect(e.message).toBe('id token is invalid for this user.');
}
});
it('should not decode invalid id_token', async () => {
try {
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('provided token does not decode as JWT');
}
});
it('should throw error if public key used to encode token is not available', async () => {
const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
try {
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe(
`Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
);
}
});
it('should use algorithm from key header to verify id_token', async () => {
const fakeClaim = {
iss: 'https://appleid.apple.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
const result = await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
expect(result).toEqual(fakeClaim);
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(
fakeDecodedToken.header.alg
);
});
it('should not verify invalid id_token', async () => {
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
try {
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('jwt malformed');
}
});
it('(using client id as array) should not verify invalid id_token', async () => {
try {
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ client_id: ['secret'] }
);
fail();
} catch (e) {
expect(e.message).toBe('provided token does not decode as JWT');
}
});
it('(using client id as string) should verify id_token', async () => {
const fakeClaim = {
iss: 'https://appleid.apple.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const result = await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
expect(result).toEqual(fakeClaim);
});
it('(using client id as array) should verify id_token', async () => {
const fakeClaim = {
iss: 'https://appleid.apple.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const result = await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: ['secret'] }
);
expect(result).toEqual(fakeClaim);
});
it('(using client id as array with multiple items) should verify id_token', async () => {
const fakeClaim = {
iss: 'https://appleid.apple.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const result = await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: ['secret', 'secret 123'] }
);
expect(result).toEqual(fakeClaim);
});
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
const fakeClaim = {
iss: 'https://not.apple.com',
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://appleid.apple.com | from: https://not.apple.com'
);
}
});
// TODO: figure out a way to generate our own apple signed tokens, perhaps with a parse apple account
// and a private key
xit('(using client id as array) should throw error with with invalid jwt issuer', async () => {
const fakeClaim = {
iss: 'https://not.apple.com',
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
await apple.validateAuthData(
{
id: 'INSERT ID HERE',
token: 'INSERT APPLE TOKEN HERE WITH INVALID JWT ISSUER',
},
{ clientId: ['INSERT CLIENT ID HERE'] }
);
fail();
} catch (e) {
expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://appleid.apple.com | from: https://not.apple.com'
);
}
});
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
const fakeClaim = {
iss: 'https://not.apple.com',
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
await apple.validateAuthData(
{
id: 'INSERT ID HERE',
token: 'INSERT APPLE TOKEN HERE WITH INVALID JWT ISSUER',
},
{ clientId: 'INSERT CLIENT ID HERE' }
);
fail();
} catch (e) {
expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://appleid.apple.com | from: https://not.apple.com'
);
}
});
// TODO: figure out a way to generate our own apple signed tokens, perhaps with a parse apple account
// and a private key
xit('(using client id as string) should throw error with invalid jwt client_id', async () => {
try {
await apple.validateAuthData(
{ id: 'INSERT ID HERE', token: 'INSERT APPLE TOKEN HERE' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('jwt audience invalid. expected: secret');
}
});
// TODO: figure out a way to generate our own apple signed tokens, perhaps with a parse apple account
// and a private key
xit('(using client id as array) should throw error with invalid jwt client_id', async () => {
try {
await apple.validateAuthData(
{ id: 'INSERT ID HERE', token: 'INSERT APPLE TOKEN HERE' },
{ clientId: ['secret'] }
);
fail();
} catch (e) {
expect(e.message).toBe('jwt audience invalid. expected: secret');
}
});
// TODO: figure out a way to generate our own apple signed tokens, perhaps with a parse apple account
// and a private key
xit('should throw error with invalid user id', async () => {
try {
await apple.validateAuthData(
{ id: 'invalid user', token: 'INSERT APPLE TOKEN HERE' },
{ clientId: 'INSERT CLIENT ID HERE' }
);
fail();
} catch (e) {
expect(e.message).toBe('auth data is invalid for this user.');
}
});
it('should throw error with with invalid user id', async () => {
const fakeClaim = {
iss: 'https://appleid.apple.com',
aud: 'invalid_client_id',
sub: 'a_different_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('auth data is invalid for this user.');
}
});
});
describe('Apple Game Center Auth adapter', () => {
const gcenter = require('../lib/Adapters/Auth/gcenter');
it('validateAuthData should validate', async () => {
// real token is used
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
timestamp: 1565257031287,
signature:
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};
try {
await gcenter.validateAuthData(authData);
} catch (e) {
fail();
}
});
it('validateAuthData invalid signature id', async () => {
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('Apple Game Center - invalid signature');
}
});
it('validateAuthData invalid public key url', async () => {
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'invalid.com',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe(
'Apple Game Center - invalid publicKeyUrl: invalid.com'
);
}
});
});
describe('phant auth adapter', () => {
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
it('validateAuthData should throw for invalid auth', async () => {
const authData = {
id: 'fakeid',
access_token: 'sometoken',
};
const { adapter } = authenticationLoader.loadAuthAdapter('phantauth', {});
spyOn(httpsRequest, 'get').and.callFake(() =>
Promise.resolve({ sub: 'invalidID' })
);
try {
await adapter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('PhantAuth auth is invalid for this user.');
}
});
});
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(101);
expect(err.message).toBe(
'Microsoft Graph auth is invalid for this user.'
);
done();
});
});
});