From 46d1430023b1677a8fb7e9c3d3afb0f7f5f5a978 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 23 Oct 2014 11:10:39 -0700 Subject: [PATCH] Add realm support --- common/models/application.json | 3 + common/models/user.js | 87 ++++++++++++++--- test/user.test.js | 174 +++++++++++++++++++++++++++++++-- 3 files changed, 247 insertions(+), 17 deletions(-) diff --git a/common/models/application.json b/common/models/application.json index 8b053ced..46213534 100644 --- a/common/models/application.json +++ b/common/models/application.json @@ -5,6 +5,9 @@ "type": "string", "id": true }, + "realm": { + "type": "string" + }, "name": { "type": "string", "required": true diff --git a/common/models/user.js b/common/models/user.js index d4a28a5c..cd3ec3af 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -57,6 +57,57 @@ User.prototype.createAccessToken = function(ttl, cb) { }, cb); }; +function splitPrincipal(name, realmDelimiter) { + var parts = [null, name]; + if(!realmDelimiter) { + return parts; + } + var index = name.indexOf(realmDelimiter); + if (index !== -1) { + parts[0] = name.substring(0, index); + parts[1] = name.substring(index + realmDelimiter.length); + } + return parts; +} + +/** + * Normalize the credentials + * @param {Object} credentials The credential object + * @param {Boolean} realmRequired + * @param {String} realmDelimiter The realm delimiter, if not set, no realm is needed + * @returns {Object} The normalized credential object + */ +User.normalizeCredentials = function(credentials, realmRequired, realmDelimiter) { + var query = {}; + credentials = credentials || {}; + if(!realmRequired) { + if (credentials.email) { + query.email = credentials.email; + } else if (credentials.username) { + query.username = credentials.username; + } + } else { + if (credentials.realm) { + query.realm = credentials.realm; + } + var parts; + if (credentials.email) { + parts = splitPrincipal(credentials.email, realmDelimiter); + query.email = parts[1]; + if (parts[0]) { + query.realm = parts[0]; + } + } else if (credentials.username) { + parts = splitPrincipal(credentials.username, realmDelimiter); + query.username = parts[1]; + if (parts[0]) { + query.realm = parts[0]; + } + } + } + return query; +} + /** * Login a user by with the given `credentials`. * @@ -88,16 +139,25 @@ User.login = function(credentials, include, fn) { include = include.toLowerCase(); } + var realmDelimiter; + // Check if realm is required + var realmRequired = !!(self.settings.realmRequired || + self.settings.realmDelimiter); + if (realmRequired) { + realmDelimiter = self.settings.realmDelimiter; + } + var query = self.normalizeCredentials(credentials, realmRequired, + realmDelimiter); - var query = {}; - if (credentials.email) { - query.email = credentials.email; - } else if (credentials.username) { - query.username = credentials.username; - } else { - var err = new Error('username or email is required'); - err.statusCode = 400; - return fn(err); + if(realmRequired && !query.realm) { + var err1 = new Error('realm is required'); + err1.statusCode = 400; + return fn(err1); + } + if (!query.email && !query.username) { + var err2 = new Error('username or email is required'); + err2.statusCode = 400; + return fn(err2); } self.findOne({where: query}, function(err, user) { @@ -488,9 +548,14 @@ User.setup = function() { // email validation regex var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - UserModel.validatesUniquenessOf('email', {message: 'Email already exists'}); + UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'}); - UserModel.validatesUniquenessOf('username', {message: 'User already exists'}); + + // FIXME: We need to add support for uniqueness of composite keys in juggler + if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) { + UserModel.validatesUniquenessOf('email', {message: 'Email already exists'}); + UserModel.validatesUniquenessOf('username', {message: 'User already exists'}); + } return UserModel; } diff --git a/test/user.test.js b/test/user.test.js index c08465a0..f0992b25 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -292,6 +292,12 @@ describe('User', function(){ }); }); + function assertGoodToken(accessToken) { + assert(accessToken.userId); + assert(accessToken.id); + assert.equal(accessToken.id.length, 64); + } + describe('User.login requiring email verification', function() { beforeEach(function() { User.settings.emailVerificationRequired = true; @@ -310,9 +316,7 @@ describe('User', function(){ it('Login a user by with email verification', function(done) { User.login(validCredentialsEmailVerified, function (err, accessToken) { - assert(accessToken.userId); - assert(accessToken.id); - assert.equal(accessToken.id.length, 64); + assertGoodToken(accessToken); done(); }); }); @@ -327,9 +331,7 @@ describe('User', function(){ if(err) return done(err); var accessToken = res.body; - assert(accessToken.userId); - assert(accessToken.id); - assert.equal(accessToken.id.length, 64); + assertGoodToken(accessToken); assert(accessToken.user === undefined); done(); @@ -348,6 +350,166 @@ describe('User', function(){ }); }); + + describe('User.login requiring realm', function() { + var User, AccessToken; + + before(function() { + User = loopback.User.extend('RealmUser', {}, + {realmRequired: true, realmDelimiter: ':'}); + AccessToken = loopback.AccessToken.extend('RealmAccessToken'); + + loopback.autoAttach(); + + // Update the AccessToken relation to use the subclass of User + AccessToken.belongsTo(User); + User.hasMany(AccessToken); + + // allow many User.afterRemote's to be called + User.setMaxListeners(0); + }); + + var realm1User = { + realm: 'realm1', + username: 'foo100', + email: 'foo100@bar.com', + password: 'pass100' + }; + + var realm2User = { + realm: 'realm2', + username: 'foo100', + email: 'foo100@bar.com', + password: 'pass200' + }; + + var credentialWithoutRealm = { + username: 'foo100', + email: 'foo100@bar.com', + password: 'pass100' + }; + + var credentialWithBadPass = { + realm: 'realm1', + username: 'foo100', + email: 'foo100@bar.com', + password: 'pass001' + }; + + var credentialWithBadRealm = { + realm: 'realm3', + username: 'foo100', + email: 'foo100@bar.com', + password: 'pass100' + }; + + var credentialWithRealm = { + realm: 'realm1', + username: 'foo100', + password: 'pass100' + }; + + var credentialRealmInUsername = { + username: 'realm1:foo100', + password: 'pass100' + }; + + var credentialRealmInEmail = { + email: 'realm1:foo100@bar.com', + password: 'pass100' + }; + + var user1; + beforeEach(function(done) { + User.create(realm1User, function(err, u) { + if (err) { + return done(err); + } + user1 = u; + User.create(realm2User, done); + }); + }); + + afterEach(function(done) { + User.deleteAll({realm: 'realm1'}, function(err) { + if (err) { + return done(err); + } + User.deleteAll({realm: 'realm2'}, done); + }); + }); + + it('rejects a user by without realm', function(done) { + User.login(credentialWithoutRealm, function(err, accessToken) { + assert(err); + done(); + }); + }); + + it('rejects a user by with bad realm', function(done) { + User.login(credentialWithBadRealm, function(err, accessToken) { + assert(err); + done(); + }); + }); + + it('rejects a user by with bad pass', function(done) { + User.login(credentialWithBadPass, function(err, accessToken) { + assert(err); + done(); + }); + }); + + it('logs in a user by with realm', function(done) { + User.login(credentialWithRealm, function(err, accessToken) { + assertGoodToken(accessToken); + assert.equal(accessToken.userId, user1.id); + done(); + }); + }); + + it('logs in a user by with realm in username', function(done) { + User.login(credentialRealmInUsername, function(err, accessToken) { + assertGoodToken(accessToken); + assert.equal(accessToken.userId, user1.id); + done(); + }); + }); + + it('logs in a user by with realm in email', function(done) { + User.login(credentialRealmInEmail, function(err, accessToken) { + assertGoodToken(accessToken); + assert.equal(accessToken.userId, user1.id); + done(); + }); + }); + + describe('User.login with realmRequired but no realmDelimiter', function() { + before(function() { + User.settings.realmDelimiter = undefined; + }); + + after(function() { + User.settings.realmDelimiter = ':'; + }); + + it('logs in a user by with realm', function(done) { + User.login(credentialWithRealm, function(err, accessToken) { + assertGoodToken(accessToken); + assert.equal(accessToken.userId, user1.id); + done(); + }); + }); + + it('rejects a user by with realm in email if realmDelimiter is not set', + function(done) { + User.login(credentialRealmInEmail, function(err, accessToken) { + assert(err); + done(); + }); + }); + }); + }); describe('User.logout', function() { it('Logout a user by providing the current accessToken id (using node)', function(done) {