How to directly call passport.serializeUser (and avoid calling authenticate altogether)?

StackOverflow https://stackoverflow.com/questions/23272948

  •  09-07-2023
  •  | 
  •  

Question

The question:

In a nutshell - how can I invoke the passport serializeUser function directly ? I am happy with the registered implementaiton I have provided, i just need to be able to invoke it without the indirection of calling authenticate in order to do so.

passport.serializeUser(function (user, done) {
    if (user) {
        done(null, user);
    }
});

As an already authenticated user, inside a Controller, I want to be able to do something like:

var user = req.user;   // get the already authenticated user out of the session
user.roles = ['agency']
passport.serializeUser(user);   // persist my changes in session storage

deserializeUser will know how to handle the changes.

Background:

Have a working MEAN Stack application using Passport for authentication with a local strategy. Everything works great - the session management is externalized to Redis. However, there is a requirement in the application to be able to "LOGON AS" another type of user to the System. An administrator (user with admin role) can for example log on as one of the users. In effect, I want this to be an illusion - all i will do is alter a few settings about the admin (role information etc and store that in session - the altered data is stored in a stack (js array - memento pattern concept) - and when the admin does a user logoff i pop the original info about the admin back into his session data.

So really what I want is the ability to tweak a few User details about the Admin user already logged in and update their session information with these changes (serialize them) without re-authenticating (apart from anything else I don't have the unencrypted password to call authenticate so that is not an option). The admin is already authenticated. - on deserializeUser the logic will check for enriched data, and if present do the right thing when deserializing the data to keep the impression that the admin is the assumed user identity.

Update:

Ok, so it looks like the behaviour is very different to what I originally expected. As follows:

1). passport.serializeUser appears to only ever be called on authenticate (login). passport.deserializeUser however gets called on every request, and often several times - therefore if putting a DB query in there, expect several queries per request to be possible.

2). req.user is the object that gets updated by passport.deserializeUser. This is DISTINCT from req._passport.session.user !! They are not the same...

3). req._passport.session.user is what updates the session store directly. Hence if you change what req._passport.session.user points at, or alter its attributes, they will internally update the session storage - I can see this behaviour by looking at the session state in Redis DB.

To solve my problem above, i don't actually need to directly call passport.serializeUser. I only need to update the req._passport.session.user with the attribute information i want persisted to session storage. But here is a key point - I believe the attributes that can be updated must match attributes that were already present when passport.serializeUser was originally used to serialize out the initial state. That last point is to be confirmed.

So I can update req._passport.session.user, and it gets persisted to session storage so long as it abides to attributes already stored in the session. Then I can override the behaviour of passport.deserializeUser to use what is passed in as an argument. Again, here is a confusing point - what is passed in as a user argument to passport.deserializeUser is the req._passport.session.user object, and what eventually gets returned by passport.deserializeUser is the req.user object accessible later on.

Was it helpful?

Solution 2

Ok, so Passport is really flexible here and accommodates exactly what I needed. Just need to first get up to speed with how it has been implemented internally. Here, I shall call the solution "Parasitic Session Polymorphism" because of the way it uses the existing session, but injects just enough new information to appear to be a different user type etc at runtime. So the solution is basically (with custom code that would need to be swapped out if anyone wishes to use the basic idea):

passport.serializeUser(function (user, done) {
    if (user) {
        var identityStack = user.identityStack || [];
        var parentId = user.parentId || '';
        var userStorage = {
            _id: user._id,
            identityStack: identityStack,
            entityId: user.entityId,
            entityType: user.entityType,
            roles: user.roles,
            parentId: parentId
        };
        done(null, userStorage);
    }
});


passport.deserializeUser(function (userStorage, done) {
    var id = userStorage._id;
    User.findOne({_id: id}).exec(function (err, user) {
        if (user) {
            if(userStorage.identityStack && userStorage.identityStack.length > 0) {
                user.identityStack = userStorage.identityStack;
                user.entityId = userStorage.entityId;
                user.entityType = userStorage.entityType;
                user.roles = userStorage.roles;
                user.parentId = userStorage.parentId
            }
            return done(null, user);
        } else {
            return done(null, false);
        }
    });
});

Then in order to actually call the passport.serializeUser from within your own code, you can achieve the required result by currying the http req object. For my implementation I push and pop the added information off a javascript array associated with .prototype as an extension to the Mongoose User Model:

PUSH KEY CODE:

            var memento = {
                entityId : agency.id,
                entityType: 'user',
                roles: ['admin', 'user']
            };
            var user = req.user;
            user.pushIdentity(memento);
            var doneWrapper = function (req) {
                var done = function (err, user) {
                    if(err) {
                        console.log('Error occurred serializing user');
                        return;
                    }
                    req._passport.session.user = user;
                    console.log('user serialized successfully');
                    return;
                };
                return done;
            };
            req._passport.instance.serializeUser(user, doneWrapper(req));
            return res.send({success: true, user: req.user});

and POP KEY CODE:

 var user = req.user;
    user.popIdentity();
    var doneWrapper = function (req) {
        var done = function (err, user) {
            if(err) {
                console.log('Error occurred serializing user');
                return;
            }
            req._passport.session.user = user;
            console.log('user serialized successfully');
            return;
        };
        return done;
    };
    req._passport.instance.serializeUser(user, doneWrapper(req));
    return res.send({success: true, user: user});

Clearly the code is not yet production ready, move out the console.log statements to a logging strategy and so on but that is the basic concept. The signature to the serializeUser method is defined in the Passport github code base under lib/authenticator.js.

KEY PART:

 Authenticator.prototype.serializeUser = function(fn, req, done) {
 if (typeof fn === 'function') {
    return this._serializers.push(fn);
 }

// private implementation that traverses the chain of serializers, attempting
// to serialize a user
var user = fn;

// For backwards compatibility
if (typeof req === 'function') {
  done = req;
   = undefined;
}

Thanks to Biba for his contributions, really appreciated and assisted in getting to a solution.

OTHER TIPS

https://github.com/jaredhanson/passport/blob/master/lib/http/request.js

function here....
...
var self = this;
    this._passport.instance.serializeUser(user, this, function(err, obj) {
      if (err) { self[property] = null; return done(err); }
      self._passport.session.user = obj;
      done();
    });
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top