1824 lines
58 KiB
JavaScript
1824 lines
58 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');
|
|
|
|
describe('AuthenticationProviders', function () {
|
|
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('should support loginWith with session token and with/without mutated authData', async () => {
|
|
const fakeAuthProvider = {
|
|
validateAppId: () => Promise.resolve(),
|
|
validateAuthData: () => Promise.resolve(),
|
|
};
|
|
const payload = { authData: { id: 'user1', token: 'fakeToken' } };
|
|
const payload2 = { authData: { id: 'user1', token: 'fakeToken2' } };
|
|
await reconfigureServer({ auth: { fakeAuthProvider } });
|
|
const user = await Parse.User.logInWith('fakeAuthProvider', payload);
|
|
const user2 = await Parse.User.logInWith('fakeAuthProvider', payload, {
|
|
sessionToken: user.getSessionToken(),
|
|
});
|
|
const user3 = await Parse.User.logInWith('fakeAuthProvider', payload2, {
|
|
sessionToken: user2.getSessionToken(),
|
|
});
|
|
expect(user.id).toEqual(user2.id);
|
|
expect(user.id).toEqual(user3.id);
|
|
});
|
|
|
|
it('should support sync/async validateAppId', async () => {
|
|
const syncProvider = {
|
|
validateAppId: () => true,
|
|
appIds: 'test',
|
|
validateAuthData: () => Promise.resolve(),
|
|
};
|
|
const asyncProvider = {
|
|
appIds: 'test',
|
|
validateAppId: () => Promise.resolve(true),
|
|
validateAuthData: () => Promise.resolve(),
|
|
};
|
|
const payload = { authData: { id: 'user1', token: 'fakeToken' } };
|
|
const syncSpy = spyOn(syncProvider, 'validateAppId');
|
|
const asyncSpy = spyOn(asyncProvider, 'validateAppId');
|
|
|
|
await reconfigureServer({ auth: { asyncProvider, syncProvider } });
|
|
const user = await Parse.User.logInWith('asyncProvider', payload);
|
|
const user2 = await Parse.User.logInWith('syncProvider', payload);
|
|
expect(user.getSessionToken()).toBeDefined();
|
|
expect(user2.getSessionToken()).toBeDefined();
|
|
expect(syncSpy).toHaveBeenCalledTimes(1);
|
|
expect(asyncSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
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 throw error when Facebook request appId is wrong data type', async () => {
|
|
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
|
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
return Promise.resolve({ id: 'a' });
|
|
});
|
|
const options = {
|
|
facebook: {
|
|
appIds: 'abcd',
|
|
appSecret: 'secret_sauce',
|
|
},
|
|
};
|
|
const authData = {
|
|
access_token: 'badtoken',
|
|
};
|
|
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
|
'facebook',
|
|
options
|
|
);
|
|
await expectAsync(adapter.validateAppId(appIds, authData, providerOptions)).toBeRejectedWith(
|
|
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.')
|
|
);
|
|
});
|
|
|
|
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('can disable provider', async () => {
|
|
await reconfigureServer({
|
|
auth: {
|
|
myoauth: {
|
|
enabled: false,
|
|
module: path.resolve(__dirname, 'support/myoauth'), // relative path as it's run from src
|
|
},
|
|
},
|
|
});
|
|
const provider = getMockMyOauthProvider();
|
|
Parse.User._registerAuthenticationProvider(provider);
|
|
await expectAsync(Parse.User._logInWith('myoauth')).toBeRejectedWith(
|
|
new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, 'This authentication method is unsupported.')
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('google auth adapter', () => {
|
|
const google = require('../lib/Adapters/Auth/google');
|
|
const jwt = require('jsonwebtoken');
|
|
const authUtils = require('../lib/Adapters/Auth/utils');
|
|
|
|
it('should throw error with missing id_token', async () => {
|
|
try {
|
|
await google.validateAuthData({}, {});
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('id token is invalid for this user.');
|
|
}
|
|
});
|
|
|
|
it('should not decode invalid id_token', async () => {
|
|
try {
|
|
await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
|
|
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(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
|
|
// await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
|
|
// fail();
|
|
// } catch (e) {
|
|
// expect(e.message).toBe(
|
|
// `Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
|
|
// );
|
|
// }
|
|
// });
|
|
|
|
it('(using client id as string) should verify id_token (google.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://accounts.google.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
const result = await google.validateAuthData(
|
|
{ id: 'the_user_id', id_token: 'the_token' },
|
|
{ clientId: 'secret' }
|
|
);
|
|
expect(result).toEqual(fakeClaim);
|
|
});
|
|
|
|
it('(using client id as string) should throw error with with invalid jwt issuer (google.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://not.google.com',
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
try {
|
|
await google.validateAuthData(
|
|
{ id: 'the_user_id', id_token: 'the_token' },
|
|
{ clientId: 'secret' }
|
|
);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe(
|
|
'id token not issued by correct provider - expected: accounts.google.com or https://accounts.google.com | from: https://not.google.com'
|
|
);
|
|
}
|
|
});
|
|
|
|
xit('(using client id as string) should throw error with invalid jwt client_id', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://accounts.google.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
try {
|
|
await google.validateAuthData(
|
|
{ id: 'INSERT ID HERE', token: 'INSERT APPLE TOKEN HERE' },
|
|
{ clientId: 'secret' }
|
|
);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('jwt audience invalid. expected: secret');
|
|
}
|
|
});
|
|
|
|
xit('should throw error with invalid user id', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://accounts.google.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
try {
|
|
await google.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.');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('keycloak auth adapter', () => {
|
|
const keycloak = require('../lib/Adapters/Auth/keycloak');
|
|
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
|
|
|
it('validateAuthData should fail without access token', async () => {
|
|
const authData = {
|
|
id: 'fakeid',
|
|
};
|
|
try {
|
|
await keycloak.validateAuthData(authData);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('Missing access token and/or User id');
|
|
}
|
|
});
|
|
|
|
it('validateAuthData should fail without user id', async () => {
|
|
const authData = {
|
|
access_token: 'sometoken',
|
|
};
|
|
try {
|
|
await keycloak.validateAuthData(authData);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('Missing access token and/or User id');
|
|
}
|
|
});
|
|
|
|
it('validateAuthData should fail without config', async () => {
|
|
const options = {
|
|
keycloak: {
|
|
config: null,
|
|
},
|
|
};
|
|
const authData = {
|
|
id: 'fakeid',
|
|
access_token: 'sometoken',
|
|
};
|
|
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter('keycloak', options);
|
|
try {
|
|
await adapter.validateAuthData(authData, providerOptions);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('Missing keycloak configuration');
|
|
}
|
|
});
|
|
|
|
it('validateAuthData should fail connect error', async () => {
|
|
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
return Promise.reject({
|
|
text: JSON.stringify({ error: 'hosting_error' }),
|
|
});
|
|
});
|
|
const options = {
|
|
keycloak: {
|
|
config: {
|
|
'auth-server-url': 'http://example.com',
|
|
realm: 'new',
|
|
},
|
|
},
|
|
};
|
|
const authData = {
|
|
id: 'fakeid',
|
|
access_token: 'sometoken',
|
|
};
|
|
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter('keycloak', options);
|
|
try {
|
|
await adapter.validateAuthData(authData, providerOptions);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('Could not connect to the authentication server');
|
|
}
|
|
});
|
|
|
|
it('validateAuthData should fail with error description', async () => {
|
|
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
return Promise.reject({
|
|
text: JSON.stringify({ error_description: 'custom error message' }),
|
|
});
|
|
});
|
|
const options = {
|
|
keycloak: {
|
|
config: {
|
|
'auth-server-url': 'http://example.com',
|
|
realm: 'new',
|
|
},
|
|
},
|
|
};
|
|
const authData = {
|
|
id: 'fakeid',
|
|
access_token: 'sometoken',
|
|
};
|
|
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter('keycloak', options);
|
|
try {
|
|
await adapter.validateAuthData(authData, providerOptions);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('custom error message');
|
|
}
|
|
});
|
|
|
|
it('validateAuthData should fail with invalid auth', async () => {
|
|
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
return Promise.resolve({});
|
|
});
|
|
const options = {
|
|
keycloak: {
|
|
config: {
|
|
'auth-server-url': 'http://example.com',
|
|
realm: 'new',
|
|
},
|
|
},
|
|
};
|
|
const authData = {
|
|
id: 'fakeid',
|
|
access_token: 'sometoken',
|
|
};
|
|
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter('keycloak', options);
|
|
try {
|
|
await adapter.validateAuthData(authData, providerOptions);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('Invalid authentication');
|
|
}
|
|
});
|
|
|
|
it('validateAuthData should fail with invalid groups', async () => {
|
|
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
return Promise.resolve({
|
|
data: {
|
|
sub: 'fakeid',
|
|
roles: ['role1'],
|
|
groups: ['unknown'],
|
|
},
|
|
});
|
|
});
|
|
const options = {
|
|
keycloak: {
|
|
config: {
|
|
'auth-server-url': 'http://example.com',
|
|
realm: 'new',
|
|
},
|
|
},
|
|
};
|
|
const authData = {
|
|
id: 'fakeid',
|
|
access_token: 'sometoken',
|
|
roles: ['role1'],
|
|
groups: ['group1'],
|
|
};
|
|
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter('keycloak', options);
|
|
try {
|
|
await adapter.validateAuthData(authData, providerOptions);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('Invalid authentication');
|
|
}
|
|
});
|
|
|
|
it('validateAuthData should fail with invalid roles', async () => {
|
|
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
return Promise.resolve({
|
|
data: {
|
|
sub: 'fakeid',
|
|
roles: 'unknown',
|
|
groups: ['group1'],
|
|
},
|
|
});
|
|
});
|
|
const options = {
|
|
keycloak: {
|
|
config: {
|
|
'auth-server-url': 'http://example.com',
|
|
realm: 'new',
|
|
},
|
|
},
|
|
};
|
|
const authData = {
|
|
id: 'fakeid',
|
|
access_token: 'sometoken',
|
|
roles: ['role1'],
|
|
groups: ['group1'],
|
|
};
|
|
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter('keycloak', options);
|
|
try {
|
|
await adapter.validateAuthData(authData, providerOptions);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('Invalid authentication');
|
|
}
|
|
});
|
|
|
|
it('validateAuthData should handle authentication', async () => {
|
|
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
return Promise.resolve({
|
|
data: {
|
|
sub: 'fakeid',
|
|
roles: ['role1'],
|
|
groups: ['group1'],
|
|
},
|
|
});
|
|
});
|
|
const options = {
|
|
keycloak: {
|
|
config: {
|
|
'auth-server-url': 'http://example.com',
|
|
realm: 'new',
|
|
},
|
|
},
|
|
};
|
|
const authData = {
|
|
id: 'fakeid',
|
|
access_token: 'sometoken',
|
|
roles: ['role1'],
|
|
groups: ['group1'],
|
|
};
|
|
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter('keycloak', options);
|
|
await adapter.validateAuthData(authData, providerOptions);
|
|
expect(httpsRequest.get).toHaveBeenCalledWith({
|
|
host: 'http://example.com',
|
|
path: '/realms/new/protocol/openid-connect/userinfo',
|
|
headers: {
|
|
Authorization: 'Bearer sometoken',
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('apple signin auth adapter', () => {
|
|
const apple = require('../lib/Adapters/Auth/apple');
|
|
const jwt = require('jsonwebtoken');
|
|
const authUtils = require('../lib/Adapters/Auth/utils');
|
|
|
|
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({}, { clientId: ['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(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
|
|
|
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 (apple.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://appleid.apple.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
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' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
|
|
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' },
|
|
{ clientId: ['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 (apple.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://appleid.apple.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
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 (apple.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://appleid.apple.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
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 (apple.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://appleid.apple.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
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 (apple.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://not.apple.com',
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
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' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
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 with token (apple.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://not.apple.com',
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
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 clientId', 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 clientId', 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 (apple.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://appleid.apple.com',
|
|
aud: 'invalid_client_id',
|
|
sub: 'a_different_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
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('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',
|
|
};
|
|
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('facebook limited auth adapter', () => {
|
|
const facebook = require('../lib/Adapters/Auth/facebook');
|
|
const jwt = require('jsonwebtoken');
|
|
const authUtils = require('../lib/Adapters/Auth/utils');
|
|
|
|
// TODO: figure out a way to run this test alongside facebook classic tests
|
|
xit('(using client id as string) should throw error with missing id_token', async () => {
|
|
try {
|
|
await facebook.validateAuthData({}, { clientId: 'secret' });
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('Facebook auth is not configured.');
|
|
}
|
|
});
|
|
|
|
// TODO: figure out a way to run this test alongside facebook classic tests
|
|
xit('(using client id as array) should throw error with missing id_token', async () => {
|
|
try {
|
|
await facebook.validateAuthData({}, { clientId: ['secret'] });
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('Facebook auth is not configured.');
|
|
}
|
|
});
|
|
|
|
it('should not decode invalid id_token', async () => {
|
|
try {
|
|
await facebook.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(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
|
|
|
await facebook.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_id('7bfa55ab-8fd7-4526-992e-6de3df16bf9c')(it)('should use algorithm from key header to verify id_token (facebook.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://www.facebook.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
const result = await facebook.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' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
|
|
try {
|
|
await facebook.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 facebook.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_id('4bcb1a1a-11f8-4e12-a3f6-73f7e25e355a')(it)('using client id as string) should verify id_token (facebook.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://www.facebook.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
const result = await facebook.validateAuthData(
|
|
{ id: 'the_user_id', token: 'the_token' },
|
|
{ clientId: 'secret' }
|
|
);
|
|
expect(result).toEqual(fakeClaim);
|
|
});
|
|
|
|
it_id('c521a272-2ac2-4d8b-b5ed-ea250336d8b1')(it)('(using client id as array) should verify id_token (facebook.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://www.facebook.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
const result = await facebook.validateAuthData(
|
|
{ id: 'the_user_id', token: 'the_token' },
|
|
{ clientId: ['secret'] }
|
|
);
|
|
expect(result).toEqual(fakeClaim);
|
|
});
|
|
|
|
it_id('e3f16404-18e9-4a87-a555-4710cfbdac67')(it)('(using client id as array with multiple items) should verify id_token (facebook.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://www.facebook.com',
|
|
aud: 'secret',
|
|
exp: Date.now(),
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
const result = await facebook.validateAuthData(
|
|
{ id: 'the_user_id', token: 'the_token' },
|
|
{ clientId: ['secret', 'secret 123'] }
|
|
);
|
|
expect(result).toEqual(fakeClaim);
|
|
});
|
|
|
|
it_id('549c33a1-3a6b-4732-8cf6-8f010ad4569c')(it)('(using client id as string) should throw error with with invalid jwt issuer (facebook.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://not.facebook.com',
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
try {
|
|
await facebook.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://www.facebook.com | from: https://not.facebook.com'
|
|
);
|
|
}
|
|
});
|
|
|
|
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook 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.facebook.com',
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
try {
|
|
await facebook.validateAuthData(
|
|
{
|
|
id: 'INSERT ID HERE',
|
|
token: 'INSERT FACEBOOK 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://www.facebook.com | from: https://not.facebook.com'
|
|
);
|
|
}
|
|
});
|
|
|
|
it('(using client id as string) with token', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://not.facebook.com',
|
|
sub: 'the_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
try {
|
|
await facebook.validateAuthData(
|
|
{
|
|
id: 'INSERT ID HERE',
|
|
token: 'INSERT FACEBOOK 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://www.facebook.com | from: https://not.facebook.com'
|
|
);
|
|
}
|
|
});
|
|
|
|
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
|
|
// and a private key
|
|
xit('(using client id as string) should throw error with invalid jwt clientId', async () => {
|
|
try {
|
|
await facebook.validateAuthData(
|
|
{
|
|
id: 'INSERT ID HERE',
|
|
token: 'INSERT FACEBOOK 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 facebook signed tokens, perhaps with a parse facebook account
|
|
// and a private key
|
|
xit('(using client id as array) should throw error with invalid jwt clientId', async () => {
|
|
try {
|
|
await facebook.validateAuthData(
|
|
{
|
|
id: 'INSERT ID HERE',
|
|
token: 'INSERT FACEBOOK 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 facebook signed tokens, perhaps with a parse facebook account
|
|
// and a private key
|
|
xit('should throw error with invalid user id', async () => {
|
|
try {
|
|
await facebook.validateAuthData(
|
|
{
|
|
id: 'invalid user',
|
|
token: 'INSERT FACEBOOK TOKEN HERE',
|
|
},
|
|
{ clientId: 'INSERT CLIENT ID HERE' }
|
|
);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.message).toBe('auth data is invalid for this user.');
|
|
}
|
|
});
|
|
|
|
it_id('c194d902-e697-46c9-a303-82c2d914473c')(it)('should throw error with with invalid user id (facebook.com)', async () => {
|
|
const fakeClaim = {
|
|
iss: 'https://www.facebook.com',
|
|
aud: 'invalid_client_id',
|
|
sub: 'a_different_user_id',
|
|
};
|
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
|
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
|
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
|
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
|
|
|
try {
|
|
await facebook.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('OTP TOTP auth adatper', () => {
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'X-Parse-Application-Id': 'test',
|
|
'X-Parse-REST-API-Key': 'rest',
|
|
};
|
|
beforeEach(async () => {
|
|
await reconfigureServer({
|
|
auth: {
|
|
mfa: {
|
|
enabled: true,
|
|
options: ['TOTP'],
|
|
algorithm: 'SHA1',
|
|
digits: 6,
|
|
period: 30,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('can enroll', async () => {
|
|
const user = await Parse.User.signUp('username', 'password');
|
|
const OTPAuth = require('otpauth');
|
|
const secret = new OTPAuth.Secret();
|
|
const totp = new OTPAuth.TOTP({
|
|
algorithm: 'SHA1',
|
|
digits: 6,
|
|
period: 30,
|
|
secret,
|
|
});
|
|
const token = totp.generate();
|
|
await user.save(
|
|
{ authData: { mfa: { secret: secret.base32, token } } },
|
|
{ sessionToken: user.getSessionToken() }
|
|
);
|
|
const response = user.get('authDataResponse');
|
|
expect(response.mfa).toBeDefined();
|
|
expect(response.mfa.recovery).toBeDefined();
|
|
expect(response.mfa.recovery.split(',').length).toEqual(2);
|
|
await user.fetch();
|
|
expect(user.get('authData').mfa).toEqual({ status: 'enabled' });
|
|
});
|
|
|
|
it('can login with valid token', async () => {
|
|
const user = await Parse.User.signUp('username', 'password');
|
|
const OTPAuth = require('otpauth');
|
|
const secret = new OTPAuth.Secret();
|
|
const totp = new OTPAuth.TOTP({
|
|
algorithm: 'SHA1',
|
|
digits: 6,
|
|
period: 30,
|
|
secret,
|
|
});
|
|
const token = totp.generate();
|
|
await user.save(
|
|
{ authData: { mfa: { secret: secret.base32, token } } },
|
|
{ sessionToken: user.getSessionToken() }
|
|
);
|
|
const response = await request({
|
|
headers,
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/login',
|
|
body: JSON.stringify({
|
|
username: 'username',
|
|
password: 'password',
|
|
authData: {
|
|
mfa: {
|
|
token: totp.generate(),
|
|
},
|
|
},
|
|
}),
|
|
}).then(res => res.data);
|
|
expect(response.objectId).toEqual(user.id);
|
|
expect(response.sessionToken).toBeDefined();
|
|
expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
|
|
expect(Object.keys(response).sort()).toEqual(
|
|
[
|
|
'objectId',
|
|
'username',
|
|
'createdAt',
|
|
'updatedAt',
|
|
'authData',
|
|
'ACL',
|
|
'sessionToken',
|
|
'authDataResponse',
|
|
].sort()
|
|
);
|
|
});
|
|
|
|
it('can change OTP with valid token', async () => {
|
|
const user = await Parse.User.signUp('username', 'password');
|
|
const OTPAuth = require('otpauth');
|
|
const secret = new OTPAuth.Secret();
|
|
const totp = new OTPAuth.TOTP({
|
|
algorithm: 'SHA1',
|
|
digits: 6,
|
|
period: 30,
|
|
secret,
|
|
});
|
|
const token = totp.generate();
|
|
await user.save(
|
|
{ authData: { mfa: { secret: secret.base32, token } } },
|
|
{ sessionToken: user.getSessionToken() }
|
|
);
|
|
|
|
const new_secret = new OTPAuth.Secret();
|
|
const new_totp = new OTPAuth.TOTP({
|
|
algorithm: 'SHA1',
|
|
digits: 6,
|
|
period: 30,
|
|
secret: new_secret,
|
|
});
|
|
const new_token = new_totp.generate();
|
|
await user.save(
|
|
{
|
|
authData: { mfa: { secret: new_secret.base32, token: new_token, old: totp.generate() } },
|
|
},
|
|
{ sessionToken: user.getSessionToken() }
|
|
);
|
|
await user.fetch({ useMasterKey: true });
|
|
expect(user.get('authData').mfa.secret).toEqual(new_secret.base32);
|
|
});
|
|
|
|
it('cannot change OTP with invalid token', async () => {
|
|
const user = await Parse.User.signUp('username', 'password');
|
|
const OTPAuth = require('otpauth');
|
|
const secret = new OTPAuth.Secret();
|
|
const totp = new OTPAuth.TOTP({
|
|
algorithm: 'SHA1',
|
|
digits: 6,
|
|
period: 30,
|
|
secret,
|
|
});
|
|
const token = totp.generate();
|
|
await user.save(
|
|
{ authData: { mfa: { secret: secret.base32, token } } },
|
|
{ sessionToken: user.getSessionToken() }
|
|
);
|
|
|
|
const new_secret = new OTPAuth.Secret();
|
|
const new_totp = new OTPAuth.TOTP({
|
|
algorithm: 'SHA1',
|
|
digits: 6,
|
|
period: 30,
|
|
secret: new_secret,
|
|
});
|
|
const new_token = new_totp.generate();
|
|
await expectAsync(
|
|
user.save(
|
|
{
|
|
authData: { mfa: { secret: new_secret.base32, token: new_token, old: '123' } },
|
|
},
|
|
{ sessionToken: user.getSessionToken() }
|
|
)
|
|
).toBeRejectedWith(new Parse.Error(Parse.Error.OTHER_CAUSE, 'Invalid MFA token'));
|
|
await user.fetch({ useMasterKey: true });
|
|
expect(user.get('authData').mfa.secret).toEqual(secret.base32);
|
|
});
|
|
|
|
it('future logins require TOTP token', async () => {
|
|
const user = await Parse.User.signUp('username', 'password');
|
|
const OTPAuth = require('otpauth');
|
|
const secret = new OTPAuth.Secret();
|
|
const totp = new OTPAuth.TOTP({
|
|
algorithm: 'SHA1',
|
|
digits: 6,
|
|
period: 30,
|
|
secret,
|
|
});
|
|
const token = totp.generate();
|
|
await user.save(
|
|
{ authData: { mfa: { secret: secret.base32, token } } },
|
|
{ sessionToken: user.getSessionToken() }
|
|
);
|
|
await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith(
|
|
new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa')
|
|
);
|
|
});
|
|
|
|
it('future logins reject incorrect TOTP token', async () => {
|
|
const user = await Parse.User.signUp('username', 'password');
|
|
const OTPAuth = require('otpauth');
|
|
const secret = new OTPAuth.Secret();
|
|
const totp = new OTPAuth.TOTP({
|
|
algorithm: 'SHA1',
|
|
digits: 6,
|
|
period: 30,
|
|
secret,
|
|
});
|
|
const token = totp.generate();
|
|
await user.save(
|
|
{ authData: { mfa: { secret: secret.base32, token } } },
|
|
{ sessionToken: user.getSessionToken() }
|
|
);
|
|
await expectAsync(
|
|
request({
|
|
headers,
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/login',
|
|
body: JSON.stringify({
|
|
username: 'username',
|
|
password: 'password',
|
|
authData: {
|
|
mfa: {
|
|
token: 'abcd',
|
|
},
|
|
},
|
|
}),
|
|
}).catch(e => {
|
|
throw e.data;
|
|
})
|
|
).toBeRejectedWith({ code: Parse.Error.SCRIPT_FAILED, error: 'Invalid MFA token' });
|
|
});
|
|
});
|
|
|
|
describe('OTP SMS auth adatper', () => {
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'X-Parse-Application-Id': 'test',
|
|
'X-Parse-REST-API-Key': 'rest',
|
|
};
|
|
let code;
|
|
let mobile;
|
|
const mfa = {
|
|
enabled: true,
|
|
options: ['SMS'],
|
|
sendSMS(smsCode, number) {
|
|
expect(smsCode).toBeDefined();
|
|
expect(number).toBeDefined();
|
|
expect(smsCode.length).toEqual(6);
|
|
code = smsCode;
|
|
mobile = number;
|
|
},
|
|
digits: 6,
|
|
period: 30,
|
|
};
|
|
beforeEach(async () => {
|
|
code = '';
|
|
mobile = '';
|
|
await reconfigureServer({
|
|
auth: {
|
|
mfa,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('can enroll', async () => {
|
|
const user = await Parse.User.signUp('username', 'password');
|
|
const sessionToken = user.getSessionToken();
|
|
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
|
await user.save({ authData: { mfa: { mobile: '+11111111111' } } }, { sessionToken });
|
|
await user.fetch({ sessionToken });
|
|
expect(user.get('authData')).toEqual({ mfa: { status: 'disabled' } });
|
|
expect(spy).toHaveBeenCalledWith(code, '+11111111111');
|
|
await user.fetch({ useMasterKey: true });
|
|
const authData = user.get('authData').mfa?.pending;
|
|
expect(authData).toBeDefined();
|
|
expect(authData['+11111111111']).toBeDefined();
|
|
expect(Object.keys(authData['+11111111111'])).toEqual(['token', 'expiry']);
|
|
|
|
await user.save({ authData: { mfa: { mobile, token: code } } }, { sessionToken });
|
|
await user.fetch({ sessionToken });
|
|
expect(user.get('authData')).toEqual({ mfa: { status: 'enabled' } });
|
|
});
|
|
|
|
it('future logins require SMS code', async () => {
|
|
const user = await Parse.User.signUp('username', 'password');
|
|
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
|
await user.save(
|
|
{ authData: { mfa: { mobile: '+11111111111' } } },
|
|
{ sessionToken: user.getSessionToken() }
|
|
);
|
|
|
|
await user.save(
|
|
{ authData: { mfa: { mobile, token: code } } },
|
|
{ sessionToken: user.getSessionToken() }
|
|
);
|
|
|
|
spy.calls.reset();
|
|
|
|
await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith(
|
|
new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa')
|
|
);
|
|
const res = await request({
|
|
headers,
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/login',
|
|
body: JSON.stringify({
|
|
username: 'username',
|
|
password: 'password',
|
|
authData: {
|
|
mfa: {
|
|
token: 'request',
|
|
},
|
|
},
|
|
}),
|
|
}).catch(e => e.data);
|
|
expect(res).toEqual({ code: Parse.Error.SCRIPT_FAILED, error: 'Please enter the token' });
|
|
expect(spy).toHaveBeenCalledWith(code, '+11111111111');
|
|
const response = await request({
|
|
headers,
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/login',
|
|
body: JSON.stringify({
|
|
username: 'username',
|
|
password: 'password',
|
|
authData: {
|
|
mfa: {
|
|
token: code,
|
|
},
|
|
},
|
|
}),
|
|
}).then(res => res.data);
|
|
expect(response.objectId).toEqual(user.id);
|
|
expect(response.sessionToken).toBeDefined();
|
|
expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
|
|
expect(Object.keys(response).sort()).toEqual(
|
|
[
|
|
'objectId',
|
|
'username',
|
|
'createdAt',
|
|
'updatedAt',
|
|
'authData',
|
|
'ACL',
|
|
'sessionToken',
|
|
'authDataResponse',
|
|
].sort()
|
|
);
|
|
});
|
|
|
|
it('partially enrolled users can still login', async () => {
|
|
const user = await Parse.User.signUp('username', 'password');
|
|
await user.save({ authData: { mfa: { mobile: '+11111111111' } } });
|
|
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
|
await Parse.User.logIn('username', 'password');
|
|
expect(spy).not.toHaveBeenCalled();
|
|
});
|
|
});
|