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(); | |
}); | |
}); | |
}); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@ernie58 @lotas I can confirm the loop. skipAfterSave solves it.