Files
kami-parse-server/spec/AuthenticationAdapters.spec.js
Florent Vilmart ff25ae254d Update parse SDK to 2.0.0 (#4925)
* WIP: Integrate JS SDK v2

- Removes backbone style callbacks
- Use Promise instead of Parse.Promise

* Fixes ParseObject and ParseRelation

* Updates Parse.Query with promises

* Alls tests should pass

* Ensure a fresh user is used for each test

* Use REST implementation to avoid side effects for username/email duplicates

* Uses js sdk v2
2018-08-05 13:58:07 -04:00

391 lines
13 KiB
JavaScript

const request = require('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() {
["facebook", "facebookaccountkit", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte"].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 authDataPromise = provider.validateAuthData({}, {});
const validateAppIdPromise = provider.validateAppId("app", "key", {});
jequal(authDataPromise.constructor, Promise.prototype.constructor);
jequal(validateAppIdPromise.constructor, Promise.prototype.constructor);
authDataPromise.then(()=>{}, ()=>{});
validateAppIdPromise.then(()=>{}, ()=>{});
done();
});
});
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 = {
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,
json: true
};
return new Promise((resolve) => {
request.post(options, (err, res, body) => {
resolve({err, res, body});
if (callback) {
callback(err, res, body);
}
});
});
}
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(({ body }) => {
expect(body.code).toBe(208);
expect(body.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']
}
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter('facebook', options);
validateAuthenticationAdapter(adapter);
expect(appIds).toEqual(['a', 'b']);
expect(providerOptions).toEqual(options.facebook);
});
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();
})
});
});