Passport: Multiple "local" strategies

Created on 7 Aug 2012  路  14Comments  路  Source: jaredhanson/passport

Hi

This is not so much a bug, but rather a question on if something can be achieved using the current implementation. Also, I'm not very proficient in passport or node, so please pardon me if I say something stupid.

Anyway, I'm trying to implement an kind of online "mini shopping mall", using express, sequelize over mysql and, ofc, passport. As usual, there's a table for customers but also a table for the several companies selling their stuff on the site.

My idea, if possible, is to have 2 LocalStrategies, one for each of the above mentioned user types. Of course, customers and sellers will have two completely different interfaces, so I was wondering if it possible to have two LocalStrategies plugged into passport, and then validate authentication based on each of them (so that if a customer tries to access the companies' backoffice pages, they get denied access).

I've checked another bug report talking about multiple strategies, but it seems to me that, once a user is logged in, passport just cares that he is authenticated, not the method he used. Also, i was left with the idea that strategies are accessed by identifier, so using two of the same type would be tricky/not possible.

Thanks in advance, and congratulations on your great work on passport
cheers

Most helpful comment

Yes, it's possible. You can use named strategies for this, as follows:

// use two LocalStrategies, registered under user and company names
passport.use('user', new LocalStrategy(
  function(username, password, done) {
    User.findOne(/* ... */)
  }
));

passport.use('company', new LocalStrategy(
  function(username, password, done) {
    Company.findOne(/* ... */)
  }
));


// authenticate using local strategies, depending on whether the route is the user or company interface
app.post('/user/login', 
  passport.authenticate('user', { successRedirect: '/user/home', failureRedirect: '/user/login' }));

app.post('/company/login', 
  passport.authenticate('company', { successRedirect: '/company/home', failureRedirect: '/company/login' }));

Does that answer your question?

All 14 comments

Yes, it's possible. You can use named strategies for this, as follows:

// use two LocalStrategies, registered under user and company names
passport.use('user', new LocalStrategy(
  function(username, password, done) {
    User.findOne(/* ... */)
  }
));

passport.use('company', new LocalStrategy(
  function(username, password, done) {
    Company.findOne(/* ... */)
  }
));


// authenticate using local strategies, depending on whether the route is the user or company interface
app.post('/user/login', 
  passport.authenticate('user', { successRedirect: '/user/home', failureRedirect: '/user/login' }));

app.post('/company/login', 
  passport.authenticate('company', { successRedirect: '/company/home', failureRedirect: '/company/login' }));

Does that answer your question?

Wow, really fast answer, thanks ;)
Only one detail missing: is there any way to prevent a valid user with valid credentials to authenticate on /user/login and, after authenticated, accessing /company/home? The examples i saw using req.isAuthenticated() don't seem to take into account what specific credentials the user has.

That falls into the category of access control, which is not something Passport deals with. This can be as simple or complex as you make it, but I would start with something like the following:

function ensureOnlyCompany(req, res, next) {
  if (isCompany(req.user)) { return next(); }
  res.redirect('/login')
}

app.get('/company/page',
  ensureOnlyCompany,
  function(req, res) {
    // serve the company page
  });

Just mount some middleware before your routes, which gates companies and users, preventing them from accessing pages they are not allowed to see.

Ok, that seems to take care of my problem ;) Thanks for your help!

How can I serialize and deserialize different users using this approach (example mentioned above: user/company)?

passport.serializeUser(function(user, done) {
  if (isUser(user)) {
    // serialize user
  } else if (isCompany(user)) {
    // serialize company
  }
});

I am using two Schemas: User and Company. I can't use this same approach in the deserializeUser method, bearing in mind that it takes id as its parameter and not user..so I can't use isUser/isCompany.

I recommend reading, lots of experimentation, and posting your questions to a forum such as Stack Overflow.

This is the place to discuss issues with the module, and I simply don't have time to provide further guidance beyond what I already have.

Cheers,
Jared

@shazly we have figured out a work around for this (sorry that it is two years too late!). What we did was in our serialize function, we used the if-then statement described above. If it was a customer, we would take their ID and prepend a "a" in front of it. For example, a customer with an id of 112 would have a serialization of a112. If it was a company, we would prepend a "b" in front of their ID. So if a company had an ID of 112, it would be b112.

In our schemas, the ID's are primary keys, so no two companies will have the same ID, nor would customers have the same ID. With this prepending of an "a" or "b" allows us to ensure each serialized ID will be unique.

This also allows us to deserialize differently based on the ID. At the start of the deserialize function, we took in the id parameter and did an if-then statement for id.charAt(0). If it was an "a" we did one thing, and if it was a "b" we did another thing.

Further, to do the database calls with an id, we did id.substr(1), to get only the id.

Hope this helps!

@shazly That's a great work around! Thank you so much for posting! I will try implementing this now. I'm adding a second type of user to a website for an event and ran into both of the problems on this thread.

UPDATE: A much cleaner solution than trying to mess with the object id's is to simplify create a new key using the id field and some other indicator. I added a userType property to my users and now do the following instead of just using the user.id:

passport.serialzeUser(function(user, done) {
  var key = {
    id: user.id,
    type: user.userType
  }
  done(null, key);
}
passport.deserializeUser(function(key, done) {
  // this could be more complex with a switch or if statements
  var Model = key.type === 'type1' ? Model1 : Model2; 
  Model.findOne({
    _id: key.id
  }, '-salt -password', function(err, user) {
    done(err, user);
  }
}

Cheers!

Hi
I have created a login medium using passportjs with mongodb, it works fine. But, now i want is to redirect pages according to the user type, i have two types of users: employee and customer. I have used only one schema in mongodb.. when registering a new user at that time only the user type is decided that the new user would be an employee or customer. I am new to nodejs.. i am confused about how i can achieve it. Please suggest.

I got same problem @noviceC....did you any solutions?

@kyle-rader your solution just worked for me now in 2018 thank you very much it saved my day

I found another solution similar to the one that @kyle-rader offered, copying code from this link below with more descriptive naming conventions. Credit to Matthew Payne:

https://stackoverflow.com/a/24336272/8211101

function SessionConstructor(userId, userGroup, details) {
    this.userId = userId;
    this.userGroup = userGroup;
    this.details = details;
}

passport.serializeUser(function (userObject, done) {
    // userObject could be a Model1 or a Model2... or Model3, Model4, etc.
    let userGroup = "model1";
    let userPrototype =  Object.getPrototypeOf(userObject);

    if (userPrototype === Model1.prototype) {
        userGroup = "model1";
    } else if (userPrototype === Resident.prototype) {
        userGroup = "model2";
    } 

    let sessionConstructor = new SessionConstructor(userObject.id, userGroup, '');
    done(null,sessionConstructor);
});

passport.deserializeUser(function (sessionConstructor, done) {

    if (sessionConstructor.userGroup == 'model1') {
        Model1.findOne({
            _id: sessionConstructor.userId
        }, '-localStrategy.password', function (err, user) { // When using string syntax, prefixing a path with - will flag that path as excluded.
            done(err, user);
        });
    } else if (sessionConstructor.userGroup == 'model2') {
        Model2.findOne({
            _id: sessionConstructor.userId
        }, '-localStrategy.password', function (err, user) { // When using string syntax, prefixing a path with - will flag that path as excluded.
            done(err, user);
        });
    } 

});

What you need to replace in the code above to integrate this with your application:

  1. "model1" and "model2" <= Unique identifiers used to add uniqueness to the session constructor
  2. "Model1" and "Model2" <= Your two different user models
  3. localStrategy.password <= Replace this with the object access key of your password and other sensitive info. See [projection] parameter of http://mongoosejs.com/docs/api.html#find_find (do not remove the single quotes around this value or the hyphen prefixed to this value from the code below)
Was this page helpful?
0 / 5 - 0 ratings

Related issues

PymZoR picture PymZoR  路  3Comments

xingrz picture xingrz  路  5Comments

AdityaAnand1 picture AdityaAnand1  路  4Comments

prakhar897 picture prakhar897  路  6Comments

andrewbanchich picture andrewbanchich  路  4Comments