Permalink
Showing
with
463 additions
and 395 deletions.
- +1 −0 docs.json
- +20 −5 lib/index.js
- +48 −72 lib/models/application-credential.js
- +25 −0 lib/models/application-credential.json
- +77 −0 lib/models/user-credential.js
- +42 −0 lib/models/user-credential.json
- +113 −232 lib/models/user-identity.js
- +43 −0 lib/models/user-identity.json
- +5 −4 lib/passport-configurator.js
- +88 −0 test/model.user-credential.test.js
- +1 −82 test/model.user-identity.test.js
1
docs.json
25
lib/index.js
| @@ -1,10 +1,25 @@ | ||
| -var uid = require('./models/user-identity'); | ||
| -exports.UserIdentity = uid.UserIdentity; | ||
| -exports.UserCredential = uid.UserCredential; | ||
| -exports.ApplicationCredential = require('./models/application-credential'); | ||
| +var loopback = require('loopback'); | ||
| +var DataModel = loopback.PersistedModel || loopback.DataModel; | ||
| + | ||
| +function loadModel(jsonFile) { | ||
| + var modelDefinition = require(jsonFile); | ||
| + return DataModel.extend(modelDefinition.name, | ||
| + modelDefinition.properties, | ||
| + { | ||
| + relations: modelDefinition.relations | ||
| + }); | ||
| +} | ||
| + | ||
| +var UserIdentityModel = loadModel('./models/user-identity.json'); | ||
| +var UserCredentialModel = loadModel('./models/user-credential.json'); | ||
| +var ApplicationCredentialModel = loadModel('./models/application-credential.json'); | ||
| + | ||
| +exports.UserIdentity = require('./models/user-identity')(UserIdentityModel); | ||
| +exports.UserCredential = require('./models/user-credential')(UserCredentialModel); | ||
| +exports.ApplicationCredential = require('./models/application-credential')(ApplicationCredentialModel); | ||
| exports.UserIdentity.autoAttach = 'db'; | ||
| exports.UserCredential.autoAttach = 'db'; | ||
| exports.ApplicationCredential.autoAttach = 'db'; | ||
| -exports.PassportConfigurator = require('./passport-configurator'); | ||
| +exports.PassportConfigurator = require('./passport-configurator'); |
120
lib/models/application-credential.js
25
lib/models/application-credential.json
| @@ -0,0 +1,25 @@ | ||
| +{ | ||
| + "name": "ApplicationCredential", | ||
| + "base": "PersistedModel", | ||
| + "properties": { | ||
| + "provider": { | ||
| + "type": "String", | ||
| + "required": true, | ||
| + "comments": "Facebook, google, twitter, linkedIn" | ||
| + }, | ||
| + "authScheme": { | ||
| + "type": "String", | ||
| + "comments": "oAuth, oAuth 2.0, OpenID, OpenID Connect" | ||
| + }, | ||
| + "credentials": "Object", | ||
| + "created": "Date", | ||
| + "modified": "Date" | ||
| + }, | ||
| + "relations": { | ||
| + "application": { | ||
| + "type": "belongsTo", | ||
| + "model": "Application", | ||
| + "foreignKey": "appId" | ||
| + } | ||
| + } | ||
| +} |
77
lib/models/user-credential.js
| @@ -0,0 +1,77 @@ | ||
| +/** | ||
| + * Tracks third-party logins and profiles. | ||
| + * | ||
| + * @param {String} provider Auth provider name, such as facebook, google, twitter, linkedin. | ||
| + * @param {String} authScheme Auth scheme, such as oAuth, oAuth 2.0, OpenID, OpenID Connect. | ||
| + * @param {String} externalId Provider specific user ID. | ||
| + * @param {Object} profile User profile, see http://passportjs.org/guide/profile. | ||
| + * @param {Object} credentials Credentials. Actual properties depend on the auth scheme being used: | ||
| + * | ||
| + * - oAuth: token, tokenSecret | ||
| + * - oAuth 2.0: accessToken, refreshToken | ||
| + * - OpenID: openId | ||
| + * - OpenID: Connect: accessToken, refreshToken, profile | ||
| + * @param {*} userId The LoopBack user ID. | ||
| + * @param {Date} created The created date | ||
| + * @param {Date} modified The last modified date | ||
| + * | ||
| + * @class | ||
| + * @inherits {DataModel} | ||
| + */ | ||
| +module.exports = function(UserCredential) { | ||
| + var utils = require('./utils'); | ||
| + | ||
| + /** | ||
| + * Link a third party account to a LoopBack user | ||
| + * @param {String} provider The provider name | ||
| + * @param {String} authScheme The authentication scheme | ||
| + * @param {Object} profile The profile | ||
| + * @param {Object} credentials The credentials | ||
| + * @param {Object} [options] The options | ||
| + * @callback {Function} cb The callback function | ||
| + * @param {Error|String} err The error object or string | ||
| + * @param {Object} [credential] The user credential object | ||
| + */ | ||
| + UserCredential.link = function (userId, provider, authScheme, profile, | ||
| + credentials, options, cb) { | ||
| + options = options || {}; | ||
| + if(typeof options === 'function' && cb === undefined) { | ||
| + cb = options; | ||
| + options = {}; | ||
| + } | ||
| + var userCredentialModel = utils.getModel(this, UserCredential); | ||
| + userCredentialModel.findOne({where: { | ||
| + userId: userId, | ||
| + provider: provider, | ||
| + externalId: profile.id | ||
| + }}, function (err, extCredential) { | ||
| + if (err) { | ||
| + return cb(err); | ||
| + } | ||
| + | ||
| + var date = new Date(); | ||
| + if (extCredential) { | ||
| + // Find the user for the given extCredential | ||
| + extCredential.credentials = credentials; | ||
| + return extCredential.updateAttributes({profile: profile, | ||
| + credentials: credentials, modified: date}, cb); | ||
| + } | ||
| + | ||
| + // Create the linked account | ||
| + userCredentialModel.create({ | ||
| + provider: provider, | ||
| + externalId: profile.id, | ||
| + authScheme: authScheme, | ||
| + profile: profile, | ||
| + credentials: credentials, | ||
| + userId: userId, | ||
| + created: date, | ||
| + modified: date | ||
| + }, function (err, i) { | ||
| + cb(err, i); | ||
| + }); | ||
| + | ||
| + }); | ||
| + } | ||
| + return UserCredential; | ||
| +}; |
42
lib/models/user-credential.json
| @@ -0,0 +1,42 @@ | ||
| +{ | ||
| + "name": "UserCredential", | ||
| + "base": "PersistedModel", | ||
| + "properties": { | ||
| + "provider": { | ||
| + "type": "String", | ||
| + "comments": "facebook, google, twitter, linkedin" | ||
| + }, | ||
| + "authScheme": { | ||
| + "type": "String", | ||
| + "comments": "oAuth, oAuth 2.0, OpenID, OpenID Connect" | ||
| + }, | ||
| + "externalId": { | ||
| + "type": "String", | ||
| + "comments": "The provider specific id" | ||
| + }, | ||
| + "profile": { | ||
| + "type": "Object" | ||
| + }, | ||
| + "credentials": { | ||
| + "type": "Object" | ||
| + }, | ||
| + "created": "Date", | ||
| + "modified": "Date" | ||
| + }, | ||
| + "acls": [{ | ||
| + "principalType": "ROLE", | ||
| + "principalId": "$everyone", | ||
| + "permission": "DENY" | ||
| + }, { | ||
| + "principalType": "ROLE", | ||
| + "principalId": "$owner", | ||
| + "permission": "ALLOW" | ||
| + }], | ||
| + "relations": { | ||
| + "user": { | ||
| + "type": "belongsTo", | ||
| + "model": "User", | ||
| + "foreignKey": "userId" | ||
| + } | ||
| + } | ||
| +} |
345
lib/models/user-identity.js
43
lib/models/user-identity.json
| @@ -0,0 +1,43 @@ | ||
| +{ | ||
| + "name": "UserIdentity", | ||
| + "plural": "UserIdentities", | ||
| + "base": "PersistedModel", | ||
| + "properties": { | ||
| + "provider": { | ||
| + "type": "String", | ||
| + "comments": "facebook, google, twitter, linkedin" | ||
| + }, | ||
| + "authScheme": { | ||
| + "type": "String", | ||
| + "comments": "oAuth, oAuth 2.0, OpenID, OpenID Connect" | ||
| + }, | ||
| + "externalId": { | ||
| + "type": "String", | ||
| + "comments": "The provider specific id" | ||
| + }, | ||
| + "profile": { | ||
| + "type": "Object" | ||
| + }, | ||
| + "credentials": { | ||
| + "type": "Object" | ||
| + }, | ||
| + "created": "Date", | ||
| + "modified": "Date" | ||
| + }, | ||
| + "acls": [{ | ||
| + "principalType": "ROLE", | ||
| + "principalId": "$everyone", | ||
| + "permission": "DENY" | ||
| + }, { | ||
| + "principalType": "ROLE", | ||
| + "principalId": "$owner", | ||
| + "permission": "ALLOW" | ||
| + }], | ||
| + "relations": { | ||
| + "user": { | ||
| + "type": "belongsTo", | ||
| + "model": "User", | ||
| + "foreignKey": "userId" | ||
| + } | ||
| + } | ||
| +} |
9
lib/passport-configurator.js
| @@ -1,6 +1,5 @@ | ||
| var loopback = require('loopback'); | ||
| var passport = require('passport'); | ||
| -var models = require('./models/user-identity'); | ||
| var _ = require('underscore'); | ||
| module.exports = PassportConfigurator; | ||
| @@ -30,9 +29,9 @@ function PassportConfigurator(app) { | ||
| PassportConfigurator.prototype.setupModels = function (options) { | ||
| options = options || {}; | ||
| // Set up relations | ||
| - this.userModel = options.userModel || loopback.getModelByType(loopback.User); | ||
| - this.userCredentialModel = options.userCredentialModel || loopback.getModelByType(models.UserCredential); | ||
| - this.userIdentityModel = options.userIdentityModel || loopback.getModelByType(models.UserIdentity); | ||
| + this.userModel = options.userModel || loopback.getModelByType(this.app.models.User); | ||
|
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
|
||
| + this.userCredentialModel = options.userCredentialModel || loopback.getModelByType(this.app.models.UserCredential); | ||
| + this.userIdentityModel = options.userIdentityModel || loopback.getModelByType(this.app.models.UserIdentity); | ||
| if (!this.userModel.relations.identities) { | ||
| this.userModel.hasMany(this.userIdentityModel, {as: 'identities'}); | ||
| @@ -362,6 +361,7 @@ PassportConfigurator.prototype.configureProvider = function (name, options) { | ||
| if (!!options.json) { | ||
| return res.json({'access_token': info.accessToken.id, userId: user.id}); | ||
| } else { | ||
| + console.log('SIGNED COOKIES PASSPORT', req.signedCookies); | ||
| res.cookie('access_token', info.accessToken.id, { signed: req.signedCookies ? true : false, | ||
| maxAge: info.accessToken.ttl }); | ||
| res.cookie('userId', user.id.toString(), { signed: req.signedCookies ? true : false, | ||
| @@ -375,6 +375,7 @@ PassportConfigurator.prototype.configureProvider = function (name, options) { | ||
| if (!!options.json) { | ||
| return res.json({'access_token': info.accessToken.id, userId: user.id}); | ||
| } else { | ||
| + console.log('SIGNED COOKIES PASSPORT', req.signedCookies); | ||
| res.cookie('access_token', info.accessToken.id, { signed: req.signedCookies ? true : false, | ||
| maxAge: info.accessToken.ttl }); | ||
| res.cookie('userId', user.id.toString(), { signed: req.signedCookies ? true : false, | ||
88
test/model.user-credential.test.js
| @@ -0,0 +1,88 @@ | ||
| +var m = require('./init'); | ||
| +var loopback = require('loopback'); | ||
| +var assert = require('assert'); | ||
| +var UserCredential = m.UserCredential; | ||
| +var User = loopback.User; | ||
| + | ||
| +before(function (done) { | ||
| + User.destroyAll(done); | ||
| +}); | ||
| + | ||
| +describe('UserCredential', function () { | ||
| + var userId = null; | ||
| + before(function (done) { | ||
| + var ds = loopback.createDataSource({ | ||
| + connector: 'memory' | ||
| + }); | ||
| + | ||
| + UserCredential.attachTo(ds); | ||
| + User.attachTo(ds); | ||
| + UserCredential.belongsTo(User); | ||
| + | ||
| + User.create({ | ||
| + username: 'facebook.abc', | ||
| + email: 'uuu@facebook.com', | ||
| + password: 'pass' | ||
| + }, function (err, user) { | ||
| + userId = user.id; | ||
| + done(err); | ||
| + }); | ||
| + }); | ||
| + | ||
| + it('supports linked 3rd party accounts', function (done) { | ||
| + UserCredential.link(userId, 'facebook', 'oAuth 2.0', | ||
| + {emails: [ | ||
| + {value: 'foo@bar.com'} | ||
| + ], id: 'f123', username: 'xyz' | ||
| + }, {accessToken: 'at1', refreshToken: 'rt1'}, function (err, cred) { | ||
| + assert(!err, 'No error should be reported'); | ||
| + | ||
| + assert.equal(cred.externalId, 'f123'); | ||
| + assert.equal(cred.provider, 'facebook'); | ||
| + assert.equal(cred.authScheme, 'oAuth 2.0'); | ||
| + assert.deepEqual(cred.credentials, {accessToken: 'at1', refreshToken: 'rt1'}); | ||
| + | ||
| + assert.equal(userId, cred.userId); | ||
| + | ||
| + // Follow the belongsTo relation | ||
| + cred.user(function (err, user) { | ||
| + assert(!err, 'No error should be reported'); | ||
| + assert.equal(user.username, 'facebook.abc'); | ||
| + assert.equal(user.email, 'uuu@facebook.com'); | ||
| + done(); | ||
| + }); | ||
| + }); | ||
| + }); | ||
| + | ||
| + it('supports linked 3rd party accounts if exists', function (done) { | ||
| + UserCredential.create({ | ||
| + externalId: 'f456', | ||
| + provider: 'facebook', | ||
| + userId: userId, | ||
| + credentials: {accessToken: 'at1', refreshToken: 'rt1'} | ||
| + }, function (err, cred) { | ||
| + UserCredential.link(userId, 'facebook', 'oAuth 2.0', | ||
| + {emails: [ | ||
| + {value: 'abc1@facebook.com'} | ||
| + ], id: 'f456', username: 'xyz' | ||
| + }, {accessToken: 'at2', refreshToken: 'rt2'}, function (err, cred) { | ||
| + assert(!err, 'No error should be reported'); | ||
| + | ||
| + assert.equal(cred.externalId, 'f456'); | ||
| + assert.equal(cred.provider, 'facebook'); | ||
| + assert.deepEqual(cred.credentials, {accessToken: 'at2', refreshToken: 'rt2'}); | ||
| + | ||
| + assert.equal(userId, cred.userId); | ||
| + | ||
| + // Follow the belongsTo relation | ||
| + cred.user(function (err, user) { | ||
| + assert(!err, 'No error should be reported'); | ||
| + assert.equal(user.username, 'facebook.abc'); | ||
| + assert.equal(user.email, 'uuu@facebook.com'); | ||
| + done(); | ||
| + }); | ||
| + }); | ||
| + }); | ||
| + }); | ||
| + | ||
| +}); |
83
test/model.user-identity.test.js
@pandaiolo Why change to reference this three models from
this.app.models?If we extend
Usermodel asAccountModel, there won't be aapp.models.User. Of cause I could pass it viaoptions, but it's not as convenient as before. So the other two models.Are these changes necessary?
Hum, your comment makes sense, but this is interesting because in my app, I actually do not have a User model, instead I have a Member model extending it. I don't remember why I changed these lines in the first place. I'll have a look in about two weeks because I will be integrating third party logins in my app, and propose a patch if needed.