Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save onel0p3z/14806aac05a0a55d16b51ef5d2ce95bf to your computer and use it in GitHub Desktop.
Save onel0p3z/14806aac05a0a55d16b51ef5d2ce95bf to your computer and use it in GitHub Desktop.
Loopback Passport Component: keep UserIdentity in sync with UserCredentials
module.exports = function (PassportUserCredential) {
/*
* Check if credentials already exist for a given provider and external id
* Enable this hook if credentials can be linked only once
*
* @param Loopback context object
* @param next middleware function
* */
PassportUserCredential.observe('before save', function checkPassportUserCredentials(ctx, next){
//new insert - see if it is used else where
if(ctx.isNewInstance === true && ctx.instance){ //indicates a new insert
var filter = {where: { provider: ctx.instance.provider, externalId: ctx.instance.externalId }};
PassportUserCredential.findOne(filter, function(err, userCredential){
if(err) return next(err);
if(userCredential){
err = new Error('Credentials already linked');
err.code = 'Validation Error';
err.statusCode = 422;
return next(err);
} else {
//allow proceed
return next();
}
});
} else {
// don't allow updates on provider and external ID
if(ctx.instance){
delete ctx.instance.externalId;
delete ctx.instance.provider;
} else if(ctx.data){
delete ctx.data.externalId;
delete ctx.data.provider;
}
next();
}
});
/*
* Keep user identities in sync after saving a user-credential
* It checks if a UserIdentityModel with the same provider and external ID exists
* It assumes that the providername of userIdentity has suffix `-login`and of userCredentials has suffix `-link`
*
* @param Loopback context object
* @param next middleware function
* */
PassportUserCredential.observe('after save', function checkPassportUserIdentities(ctx, next){
var data = JSON.parse(JSON.stringify(ctx.instance));
data.provider = data.provider.replace('-link', '-login');
delete data.id; // has to be auto-increment
var PassportUserIdentity = PassportUserCredential.app.models.PassportUserIdentity;
var filter = {where: { provider: data.provider, externalId: data.externalId }};
PassportUserIdentity.findOrCreate(filter, data, next);
});
};
module.exports = function (PassportUserIdentity) {
/*
* Keep user credentials in sync after saving a user-identity
* It checks if a UserCredentialModel with the same provider and external ID exists for that user
* It assumes that the providername of userIdentity has suffix `-login`and of userCredentials has suffix `-link`
*
* @param Loopback context object
* @param next middleware function
* */
PassportUserIdentity.observe('after save', function checkPassportUserCredentials(ctx, next){
var data = JSON.parse(JSON.stringify(ctx.instance));
data.provider = data.provider.replace('-login', '-link');
delete data.id; // has to be auto-increment
var PassportUserCredential = PassportUserIdentity.app.models.PassportUserCredential;
var filter = {where: { userId: data.userId, provider: data.provider, externalId: data.externalId }};
PassportUserCredential.findOrCreate(filter, data, next);
});
};
var app = require('../../server');
var assert = require('assert');
var loopback = require('loopback');
before(function changeDataSourceToMemory(done) {
var db = app.dataSources.db;
loopback.configureModel(loopback.getModel('PassportUserCredential'), {dataSource: db});
loopback.configureModel(loopback.getModel('PassportUserIdentity'), {dataSource: db});
loopback.configureModel(loopback.getModel('Person'), {dataSource: db});
if (db.connected) {
db.automigrate(addUser);
} else {
db.once('connected', function () {
db.automigrate(addUser);
});
}
function addUser() {
app.models.Person.create({
'id': 1,
'name': 'John',
'firstName': 'Doe',
'created': new Date(),
'gender': 'M',
'type': 'teacher',
'username': 'john-doe',
'password': 'testme',
'email': '[email protected]'
}, done);
}
});
describe('Sync credentials when adding identities', function () {
var user;
var loginProvider = 'google-login';
var linkProvider = 'google-link';
var dummyLoginData = {
'provider': loginProvider,
'authScheme': 'oAuth 2.0',
'externalId': '1081943683967445545454654646465411',
'profile': {'info': 'some-provider-info'},
'credentials': {'accessToken': 'secret-token-from-google'}
};
var dummyLinkData = {
'provider': linkProvider,
'authScheme': 'oAuth 2.0',
'externalId': '1081943683967445545454654646465411',
'profile': {'info': 'some-provider-info'},
'credentials': {'accessToken': 'secret-token-from-google'}
};
before(function (done) {
app.models.Person.findOne({id: 1}, function (err, u) {
if (err) return done(err);
user = u;
dummyLoginData.userId = user.id;
done();
});
});
describe('IDENTITIES', function(){
describe('add new identity', function () {
it('should add the identity and credentials', function (done) {
addDataAndExpectOneIdentityAndCredential(dummyLoginData, app.models.PassportUserIdentity, done);
});
});
//https://github.com/strongloop/loopback-component-passport/issues/131
describe.skip('add existing identity', function () {
it('should do nothing on both tables', function (done) {
addDataAndExpectOneIdentityAndCredential(dummyLoginData, app.models.PassportUserIdentity, done);
});
});
//https://github.com/strongloop/loopback-component-passport/issues/131
describe.skip('add existing identity with no credentials yet', function () {
it('should not add identity but add credentials', function (done) {
app.models.PassportUserCredential.destroyAll({'provider': linkProvider}, function (err) {
if (err) return done(err);
addDataAndExpectOneIdentityAndCredential(dummyLoginData, app.models.PassportUserIdentity, done);
});
});
});
describe('add new identity with already existing credentials', function () {
it('should add identity but not add credentials', function (done) {
app.models.PassportUserIdentity.destroyAll({'provider': loginProvider}, function (err) {
if (err) return done(err);
addDataAndExpectOneIdentityAndCredential(dummyLoginData, app.models.PassportUserIdentity, done);
});
});
});
});
describe('CREDENTIALS', function(){
//reset tables
before(function(done){
app.models.PassportUserIdentity.destroyAll({'provider': loginProvider}, function (err) {
if (err) return done(err);
app.models.PassportUserCredential.destroyAll({'provider': linkProvider}, done);
});
});
describe('add new credential', function () {
it('should add the credential and identity', function (done) {
addDataAndExpectOneIdentityAndCredential(dummyLinkData, app.models.PassportUserCredential, done);
});
});
describe('add existing credential', function () {
it('should fail and return a Validation error', function (done) {
app.models.PassportUserCredential.create(dummyLinkData, function (err) {
if (!err) return done('it should fail');
assert.equal(err.code, 'Validation Error');
assert.equal(err.message, 'Credentials already linked');
app.models.PassportUserIdentity.find(
{externalId: dummyLinkData.externalId, provider: loginProvider},
function (err, identities) {
if (err) return done(err);
assert.equal(identities.length, 1);
//get credentials for this provider and externalId
app.models.PassportUserCredential.find(
{externalId: dummyLinkData.externalId, provider: linkProvider},
function (err, creds) {
if (err) return done(err);
assert.equal(creds.length, 1);
done();
});
});
});
});
});
describe('add new credential with existing identity', function () {
it('should fail and return a Validation error', function (done) {
app.models.PassportUserCredential.destroyAll({'provider': linkProvider}, function (err) {
if (err) return done(err);
addDataAndExpectOneIdentityAndCredential(dummyLinkData, app.models.PassportUserCredential, done);
});
});
});
});
function addDataAndExpectOneIdentityAndCredential(data, model, done) {
model.create(data, function (err, inst) {
if (err) return done(err);
assert.equal(inst.provider, data.provider);
assert.equal(inst.externalId, data.externalId);
app.models.PassportUserIdentity.find(
{externalId: data.externalId, provider: loginProvider},
function (err, identities) {
if (err) return done(err);
assert.equal(identities.length, 1);
//get credentials for this provider and externalId
app.models.PassportUserCredential.find(
{externalId: data.externalId, provider: linkProvider},
function (err, creds) {
if (err) return done(err);
assert.equal(creds.length, 1);
done();
});
});
});
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment