Last active
May 7, 2018 08:12
-
-
Save ernie58/09cd5c065ae44e1651be08179cce49cf to your computer and use it in GitHub Desktop.
Loopback Passport Component: keep UserIdentity in sync with UserCredentials
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}); | |
}); | |
}); | |
} | |
}); |
@ernie58 Seems like loopback enters an endless loop here:
UC:after save -> save UI -> UI: after save -> save UC -> UC: after save ..
Can be fixed by adding extra option {skipAfterSave: true}
to both models:
UserIdentity.observe('after save', function checkuserCredentials(ctx, next) {
if (ctx.options && ctx.options.skipAfterSave) return 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 userCredential = UserIdentity.app.models.userCredential;
var filter = {where: {userId: data.userId, provider: data.provider, externalId: data.externalId}};
userCredential.findOrCreate(filter, data, {skipAfterSave: true}, next);
});
Nice , I'll remember that trick.
It's strange however that you have an endless loop!
I purposely used findOrCreate, so the loop should stop once a model is found, because nothing gets saved then anymore
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It assumes my models are called PassportUserCredential and PassportUserIdentity