Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to get current authenticated user #569

Closed
cajoy opened this issue Sep 18, 2014 · 75 comments
Closed

How to get current authenticated user #569

cajoy opened this issue Sep 18, 2014 · 75 comments

Comments

@cajoy
Copy link

cajoy commented Sep 18, 2014

In requeest I have valid access token so user available in beforeRemote as part of ctx

But how can I get userId from inside of function. For example:

model.js

var loopback = require('loopback');

module.exports = function(someModel) {

someModel.getUserId = function(callback) {
    // ??? How ???
    var userId = loopback.request.getUserId();
};

spent 3 hours on it and can't find easy solution... Basically how can I get user ID from any place in the code?

@meng-zhang
Copy link

In the beforeRemote, you can find the userId in ctx.res.token.userId
Not sure this is the best solution or not.

@cajoy
Copy link
Author

cajoy commented Sep 19, 2014

Yes I know it.

But what if I need it inside of function? not in hook? Or how can I pass it to the function without using global variables?

Thanks

@bajtos
Copy link
Member

bajtos commented Sep 20, 2014

I am afraid there is no way how to pass the current user to the remoted method. We are working on a solution that will make this possible - see #337

@bajtos
Copy link
Member

bajtos commented Sep 20, 2014

A hacky workaround: use the new header argument source (see strongloop/strong-remoting#104) to get the access-token from the request header, then look up the userId by querying access tokens.

@brainthinks
Copy link

For anyone else having this problem, here is my implementation of the "hacky workaround" suggested by @bajtos . I put this in my server.js file.

// Retrieve the currently authenticated user
app.use(function (req, res, next) {
  // First, get the access token, either from the url or from the headers
  var tokenId = false;
  if (req.query && req.query.access_token) {
    tokenId = req.query.access_token;
  }
  // @TODO - not sure if this is correct since I'm currently not using headers
  // to pass the access token
  else if (req.headers && req.headers.access_token) {
    // tokenId = req.headers.access_token
  }

  // Now, if there is a tokenId, use it to look up the currently authenticated
  // user and attach it to the app
  app.currentUser = false;
  if (tokenId) {
    var UserModel = app.models.User;

    // Logic borrowed from user.js -> User.logout()
    UserModel.relations.accessTokens.modelTo.findById(tokenId, function(err, accessToken) {
      if (err) return next(err);
      if ( ! accessToken) return next(new Error('could not find accessToken'));

      // Look up the user associated with the accessToken
      UserModel.findById(accessToken.userId, function (err, user) {
        if (err) return next(err);
        if ( ! user) return next(new Error('could not find a valid user'));

        app.currentUser = user;
        next();
      });

    });
  }

  // If no tokenId was found, continue without waiting
  else {
    next();
  }
});

@ffflabs
Copy link

ffflabs commented Oct 27, 2014

This is the workaround I used on a custom route /whoami

    router.get('/whoami', function (req, res) {
        var app = require('../server');
        var AccessToken = app.models.AccessToken;
        AccessToken.findForRequest(req, {}, function (aux, accesstoken) {
            console.log(aux, accesstoken);
            if (accesstoken == undefined) {
                res.status(401);
                res.send({
                    'Error': 'Unauthorized',
                    'Message': 'You need to be authenticated to access this endpoint'
                });
            } else {
                var UserModel = app.models.user;
                UserModel.findById(accesstoken.userId, function (err, user) {
                    console.log(user);
                                       res.status(200);
                                       res.send();
                });
            }
        });
    });

this route is inside /server/boot/root.js

@brainthinks
Copy link

@amenadiel - thanks for posting your solution! I didn't know about that AccessToken method findForRequest. Here is my solution updated to use that method, which solves my original TODO:

// Retrieve the currently authenticated user
app.use(function (req, res, next) {
  app.currentUser = null;

  // Retrieve the access token used in this request
  var AccessTokenModel = app.models.AccessToken;
  AccessTokenModel.findForRequest(req, {}, function (err, token) {
    if (err) return next(err);
    if ( ! token) return next(); // No need to throw an error here

    // Logic borrowed from user.js -> User.logout() to get access token object
    var UserModel = app.models.User;
    UserModel.relations.accessTokens.modelTo.findById(token.id, function(err, accessToken) {
      if (err) return next(err);
      if ( ! accessToken) return next(new Error('could not find the given token'));

      // Look up the user associated with the access token
      UserModel.findById(accessToken.userId, function (err, user) {
        if (err) return next(err);
        if ( ! user) return next(new Error('could not find a valid user with the given token'));

        app.currentUser = user;
        next();
      });

    });
  });

});

@bajtos
Copy link
Member

bajtos commented Oct 29, 2014

I don't recommend attaching the user to the app object, it will stop working once you have more than one request being handled in parallel.

Setting that aside, here is a simpler solution:

app.use(loopback.token());
app.use(function(req, res, next) {
  app.currentUser = null;
  if (!req.accessToken) return next();
  req.accessToken.user(function(err, user) {
    if (err) return next(err);
    app.currentUser = user;
    next();
  });
});

But as I said, don't do that. It creates a hard to debug bug where Request A sees app.currentUser filled by Request B in case these requests arrive at the server (almost) in parallel.

@brainthinks
Copy link

@bajtos - wow, thank you for taking the time to point this out. To take your advice and avoid this potential (serious) problem, I have attached the currentUser to the request object rather than the app object so that it is specific to the remote context, as follows:

app.use(loopback.token());
app.use(function (req, res, next) {
  if ( ! req.accessToken) return next();
  app.models.User.findById(req.accessToken.userId, function(err, user) {
    if (err) return next(err);
    if ( ! user) return next(new Error('No user with this access token was found.'));
    req.currentUser = user;
  });
});

I tried first using req.accessToken.user(), but user kept being returned as null.

Now, I am able to access this currentUser in all of my beforeRemote hooks via ctx.req.currentUser. I think this should avoid any issues of user B's credentials being available in user A's request, as each request is a different object, thus each request will have the correct user's information.

@doublemarked
Copy link

@mymainmanbrown - this helped a lot, thank you! I am quite satisfied with the end result of this thread.

Small note: your sample above is missing a final next() call after the currentUser is assigned.

@brainthinks
Copy link

@bajtos - thanks for the commit!

@doublemarked - thanks for the catch. Since implementing this in my own project, I've discovered a "more proper" way of doing this, though the steps and end result are the same. Looking through the expressjs docs, I came across the entry on res.locals, which appears to be intended to store session/state data for the current request. Thus, without further ado, here is the code I'm using now, with the next() call:

app.use(loopback.token());
app.use(function (req, res, next) {
  if ( ! req.accessToken) return next();
  app.models.User.findById(req.accessToken.userId, function(err, user) {
    if (err) return next(err);
    if ( ! user) return next(new Error('No user with this access token was found.'));
    res.locals.currentUser = user;
    next();
  });
});

This is parallel-safe and can be access anywhere you have access to ctx or res.

@bajtos
Copy link
Member

bajtos commented Nov 12, 2014

And if you want to use the new context functionality, you can store the user in the loopback context too:

app.use(loopback.context());
app.use(loopback.token());
app.use(function (req, res, next) {
  if (!req.accessToken) return next();
  app.models.User.findById(req.accessToken.userId, function(err, user) {
    if (err) return next(err);
    if (!user) return next(new Error('No user with this access token was found.'));
    res.locals.currentUser = user;
    var loopbackContext = loopback.getCurrentContext();
    if (loopbackContext) loopbackContext.set('currentUser', user);
    next();
  });
});

// anywhere in the app
var ctx = loopback.getCurrentContext();
var currentUser = ctx && ctx.get('currentUser');

@doublemarked
Copy link

Beautiful, thank you guys.

@bajtos What is this new context functionality and where is it documented? It is new as of what version?

@bajtos
Copy link
Member

bajtos commented Nov 12, 2014

What is this new context functionality and where is it documented? It is new as of what version?

It was not released yet, we are expecting to release it early next week. You can give it a try by installing loopback from github master (npm install strongloop/loopback). The documentation is in progress, it's not done yet.

@bajtos
Copy link
Member

bajtos commented Nov 12, 2014

Until we have real docs, you can learn about the feature from the pull request - see #337.

@doublemarked
Copy link

Ok, thank you very much! I enjoy your enthusiasm for upcoming features :)

@pulkitsinghal
Copy link

@mymainmanbrown - How would you access res.locals.currentUser inside a remote method?

@doublemarked
Copy link

@pulkitsinghal - You need to have a res handle, which you can pass in as a parameter when you declare the remote method. However, since the recent release we've swapped to using the loopback.getCurrentContext() method that @bajtos recommended and which does not require a res handle.

@pulkitsinghal
Copy link

@doublemarked - But I just don't know how to get at the loopback variable inside a common/models/model.js file, can you suggest?

@doublemarked
Copy link

It's a handle for the loopback module,

var loopback = require('loopback');

@pulkitsinghal
Copy link

@doublemarked - Thank You ... strange I could have sworn that failed with an earlier version of loopback but now it works inside model files after upgrade to 2.8.5 ... may I ask a follow up?
For console.log(loopback.getCurrentContext().accessToken()); it throws "message": "Object #<Namespace> has no method 'accessToken'", ... what could be going on?

@doublemarked
Copy link

Well, indeed I don't think accessToken is a valid function within the context instance. Perhaps you mean something like the following?

console.log(loopback.getCurrentContext().get('accessToken'));

Can you compare carefully your code with the examples earlier in this thread? Our implementation is almost exactly like the one given by bajtos on Nov 12.

@pulkitsinghal
Copy link

Thank you @doublemarked , I left a comment in the docs issue where I had picked up the incorrect syntax and CC'ed you on it.

@pulkitsinghal
Copy link

@doublemarked - sorry to bug you again, I'll make sure to jot down everything that I'm unclear on against the doc issue too. But I just can't seem to make some of the simplest logic work.

From the Nov 12 code:
if ( ! req.accessToken) return next();
... always ends up running! So I never get to set the context.

Why would req.accessToken be missing when I know that its specified for sure? Could it have to do with where in the server.js file the following are placed?

// -- Add your pre-processing middleware here --
app.use(loopback.context());
app.use(loopback.token());
app.use(function (req, res, next) {
  console.log('me me me me me me me me');
  console.log(req.accessToken);
  if (!req.accessToken) {
  ...

@doublemarked
Copy link

That seems like the correct placement. How are you specifying the access token? Are all of your loopback modules up to date?

@SandeshSarfare
Copy link

This is how I've implemented it
app.start = function() {
// start the web server
return app.listen(function() {
app.emit('started');
var baseUrl = app.get('url').replace(//$/, '');
console.log('Web server listening at: %s', baseUrl);
if (app.get('loopback-component-explorer')) {
var explorerPath = app.get('loopback-component-explorer').mountPath;
console.log('Browse your REST API at %s%s', baseUrl, explorerPath);
}
});
};

app.use(loopback.context());
app.use(loopback.token());
app.use(function setCurrentUser(req, res, next) {
if (!req.accessToken) {
return next();
}
app.models.User.findById(req.accessToken.userId, function(err, user) {
if (err) {
return next(err);
}
if (!user) {
return next(new Error('No user with this access token was found.'));
}
var loopbackContext = loopback.getCurrentContext();
if (loopbackContext) {
loopbackContext.set('currentUser', user);
}
next();
});
});

However i keep getting the currentUser as null,

@SandeshSarfare
Copy link

This seems like an unresolved bug.....or maybe I'm doing something horribly wrong!!!!

@drish
Copy link

drish commented Jan 26, 2016

@drmikecrowe best solution IMHO, 👍 for that.

@drish
Copy link

drish commented Jan 26, 2016

@drmikecrowe solution resolves what was asked in this issue.

@raymondfeng
Copy link
Member

To receive the http context, there are potentially two options (assuming there is no reliable way to retrieve the current context associated with the invocation chain):

  1. Via the parameter injection. What @drmikecrowe proposes supports that.
  2. Via the model class injection. You can get it using this.context.One of the limitation is that the model class is a singleton. We can relax that by supporting a setting so that we can create new (subclassing) or reuse pooled classes of the model as the receiver. That will allow the runtime to inject context into the class object itself and remote methods can access it via this.context.

@SandeshSarfare
Copy link

loopback.getCurrentContext() returns null in boot script itself...

@johannesjo
Copy link

Could someone please explain me @drmikecrowe method? I'm really not sure where the whole 'accepts' part should go.

@drmikecrowe
Copy link

@johannesjo -- I added the current user to the current context in server/server.js like @Sandy1088 above:

app.use(function setCurrentUser(req, res, next) {
    if (!req.accessToken) {
        return next();
    }
    app.models.User.findById(req.accessToken.userId, function(err, user) {
        if (err) {
            return next(err);
        }
        if (!user) {
            return next(new Error('No user with this access token was found.'));
        }
        var loopbackContext = loopback.getCurrentContext();
        if (loopbackContext) {
            req.accessToken.currentUser = user;
            loopbackContext.set('currentUser', user);
        }
        next();
    });
});

My other solution was in remote methods:

user.remoteMethod('methodName', {
        accepts: [
            {arg: 'req', type: 'object', required: true, http: {source: 'req'}}
        ],
        returns: {arg: 'user', type: 'object', root: true},
        http:    {verb: 'post', path: '/methodName'}
    }
);


user.methodName = Bluebird.promisify(function methodName(req, cb) {
    var context            = loopback.getCurrentContext();
    var currentUser        = req.accessToken && req.accessToken.currentUser || context && context.get('currentUser');
    // .....
});

@johannesjo
Copy link

@drmikecrow thank you very much for explaining!

@Mad-Head
Copy link

someModel.getUserId = function(id, cb) {
    cb(null, id);
};

someModel.getUserId(
    'getClientUser',
    {
        accepts: {arg: 'id', type: 'string'},
        returns: {arg: 'User', type: 'array'},
        http: {verb: 'get'}
    }
);

someModel.beforeRemote('getUserId', function (context, unused, next) {
    var req = context.req;

    req.remotingContext.args.id = req.accessToken.userId;

    next();
});

@mariohmol
Copy link

I'm having this problem when owner has the same id.. in example a user may want to update the profile data... so i check if the user.id in update is the same in accesstoken

user.beforeRemote('upsert', function (context, model, next) {
  var req = context.req;
  var userobject = context.req.body;
  var userID = req.accessToken.userId;
  if(userobject && userobject._id!=null && userobject._id!=req.accessToken.userId){
      var error = new Error();
      error.statusCode = 401;
      error.message = 'Authorization Required';
      error.code = 'AUTHORIZATION_REQUIRED';
      next(error);
  } else next();
 });

@SamuelBonilla
Copy link

SamuelBonilla commented Jun 15, 2016

The solution for get current authenticated user:


var loopback = require('loopback');
module.exports = function(Model) {    
    Model.observe('before save', function(ctx, next) {
         if (ctx.instance)` {
            var ctxs = loopback.getCurrentContext();
            ctx.instance.date = new Date();
            ctx.instance.id_user = ctxs.active.accessToken["userId"];
            console.log('currentUser.infotmation: ', ctxs.active.accessToken);
        } else {
        }
        next();
    });
};

remsume :

var loopback = require('loopback');
var ctxs = loopback.getCurrentContext();
ctxs.active.accessToken

// Console output for ctxs.active.accessToken


{ id: 'vcrl8INUAk99MCbTHPB8zA9fuR0gDCLGi0f4DbqYvAIoMOLPVrc4PVYuWDJLUJGv',
  ttl: 1209600,
  created: Tue Jun 14 2016 23:48:08 GMT+0000 (UTC),
  userId: 13 }

@renanwilliam
Copy link

@SamuelBonilla do you have to set any other param in server.js? I have exactly the same code and it's returning null

@SamuelBonilla
Copy link

SamuelBonilla commented Aug 3, 2016

@renanwilliam I don't have other code in serrver.js. getCurrentContext().active.accessToken return null when the user is not authenticated you need to pass the accessToken to the api endponint

@chinuy
Copy link

chinuy commented Aug 3, 2016

maybe 'downgrade' your nodejs to v0.12 and see if the issue existed. You may refer this discussion:
#878 (comment)

@jankcat
Copy link

jankcat commented Nov 2, 2016

For those looking for a solution to finding out the authenticated user in a remote method (now that currentContext is deprecated), the groundwork is laid out at the following link: https://docs.strongloop.com/display/public/LB/Remote+methods#Remotemethods-HTTPmappingofinputarguments

Here is my example snippet:

  MyObject.remoteMethod(
    'myMethod',
    {
      http: {path: '/myMethod', verb: 'get'},
      accepts: [
        {arg: 'someArgument', type: 'string'},
        {
          arg: 'accessToken',
          type: 'object',
          http: function(ctx) {
            return ctx.req.accessToken;
          }
        }
        ],
      returns: {arg: 'someResponse', type: 'string'}
    }
  );
  MyObject.myMethod = function(someArgument, accessToken, cb) {
    // ...
    // accessToken.userId
    // ...
    cb(null, "Response blah blah...");
  }

@yagobski
Copy link
Member

yagobski commented Nov 3, 2016

Any other solution to get current connected user inside the model?

@ataft
Copy link

ataft commented Jan 12, 2017

@jankcat Thanks, this example worked perfectly! Exactly what I was looking for after reading too many pages of back and forth on the issue. I couldn't figure this out from the current 3.0 documentation, so I created a pull request to add your example to the documentation.

@bajtos
Copy link
Member

bajtos commented Jan 13, 2017

@ataft thank you for the pull request to improve our documentation!

As I commented there, while the example shown in this issue is a valid one, it is also using a different approach than we implemented for context propagation.

Here is a better example:

// common/models/my-model.js
module.exports = function(MyModel) {
  MyModel.log = function(messageId, options) {
    const Message = this.app.models.Message;
    return Message.findById(messageId, options)
      .then(msg => {
        const userId = options && options.accessToken && options.accessToken.userId;
        const user = userId ? 'user#' + userId : '<anonymous>';
        console.log('(%s) %s', user, msg.text));
      });
  };

// common/models/my-model.json
{
  "name": "MyModel",
  // ...
  "methods": {
    "log": {
      "accepts": [
        {"arg": "messageId", "type": "number", "required": true},
        {"arg": "options", "type": "object", "http": "optionsFromRequest"}
      ],
      "http": {"verb": "POST", "path": "/log/:messageId"}
    }
  }
}

@jmwohl
Copy link

jmwohl commented Jan 18, 2017

I'm probably misunderstanding something here, as I'm pretty new to Loopback. But do the examples above imply that every model method that needs access to the authenticated user must implement this explicitly? This seems like a lot of overhead for an extremely common situation. Is there any way to just make the authenticated user, if it exists, available in ALL remote methods?

@bajtos
Copy link
Member

bajtos commented Jan 19, 2017

I'm probably misunderstanding something here, as I'm pretty new to Loopback. But do the examples above imply that every model method that needs access to the authenticated user must implement this explicitly?

Yes, that's correct - see http://loopback.io/doc/en/lb3/Using-current-context.html

This seems like a lot of overhead for an extremely common situation. Is there any way to just make the authenticated user, if it exists, available in ALL remote methods?

We tried to use continuation-local-storage as a Node.js replacement for ThreadLocalStorage, unfortunately it turned out there is no reliable implementation of CLS available in Node.js land :(

The solution based on "options" argument is the only robust and reliable approach we have found. However, if you can find a better way, then please let us know!

@jmwohl
Copy link

jmwohl commented Jan 19, 2017

@bajtos Ok, thanks.

I haven't looked under the hood, so not sure how complicated it would be to implement, but one thought would be to offer a setting at the model level that populates the options from request automatically for all methods. It just seems like such a common need — I can't remember ever working on an app where I didn't need access to the user in the controllers (remote methods, in this case). Maybe something like:

// common/models/my-model.json
{
  "name": "MyModel",
  "includeOptionsFromRequest": true,
  ...
}

@barocsi
Copy link

barocsi commented Jan 28, 2017

Coming from 2.x + current context I am not able to port this implementation to the following cases, and the documentations concept is soo confusing...
options object
optionsFromRequest (that is converted) what? Really? What about options not coming from a request but from an internal test run?

.getCurrentContext
.runInContext (for testing)
.set('xx',value)

the example about getting current user

// common/models/my-model.js
module.exports = function(MyModel) {
  MyModel.observe('access', function(ctx, next) {
    const token = ctx.options && ctx.options.accessToken;
    const userId = token && token.userId;
    const user = userId ? 'user#' + userId : '<anonymous>';

    const modelName = ctx.Model.modelName;
    const scope = ctx.where ? JSON.stringify(ctx.where) : '<all records>';
    console.log('%s: %s accessed %s:%s', new Date(), user, modelName, scope);
    next();
  });
};

is not in relation with
2.x middleware, it does not even return current user object but some token and thats it,

  server.middleware('auth', loopback.token({
                                             model: server.models.accessToken,
                                             currentUserLiteral: 'me'
                                           }));

according to #569 is not acceptable since all framework has this very basic foundation to simply have a shorthand for accessing the current user.

Is there an example of middleware or component for 3.x so we can simply, by using options can retrieve currentUser reference? I mean in a large app there are hundreds of remote methods, shall we need to rewrite all of them? I'm sure there is a better way other than messing up the community with bad design and another bad design not even producing a really usable documentation for cases to be replaced easily. Not even a migration guide.

@bajtos
Copy link
Member

bajtos commented Jan 30, 2017

@barocsi I am afraid I don't understand your comment very well. I'll try to answer as best as I can.

What about options not coming from a request but from an internal test run?

.getCurrentContext
.runInContext (for testing)
.set('xx',value)

When you are invoking a method from code (e.g. in a test), you need to build and pass the options object explicitly. For example:

it('does something with current user', function() {
  return User.login(CREDENTIALS)
    .then(token => {
      const options = {accessToken: token};
      return MyModel.customMethod(someArgs, options);
    })
    .then(result => {
      // verify customMethod worked as expected
    });

it does not even return current user object but some token

The options value contain the same token as set by LoopBack 2.x. You can retrieve the current user e.g. by calling options.accessToken.user() method.

I mean in a large app there are hundreds of remote methods, shall we need to rewrite all of them?

Yes, all remote methods that want to access current context must be modified to accept options argument.

The lack of a solution for ThreadLocalStorage is a limitation of Node.js platform. There is some work in progress on this front, but it's still far from getting into a state where it could be used in production. I am highly encouraging you to get involved in the discussion and work in Node.js core, here are few links to get you started:

@gdeckert
Copy link

@bajtos - thank you for this information. Two points that aren't entirely clear -

  1. to make use of options, will I need to migrate from LoopBack 2.x to LoopBack 3.0?

  2. Are only operation hooks affected by this issue ('before save', 'after save', etc), or are all remote methods subject to continuation passing issues? (i.e. would I have to update every place where I'm currently referencing req.accessToken?)

@bajtos
Copy link
Member

bajtos commented Feb 17, 2017

@gdeckert

to make use of options, will I need to migrate from LoopBack 2.x to LoopBack 3.0?

You can pass context via options in LoopBack 2.x as long as you enable this feature, see http://loopback.io/doc/en/lb3/Using-current-context.html#annotate-options-parameter-in-remoting-metadata:

In LoopBack 2.x, this feature is disabled by default for compatibility reasons. To enable, add "injectOptionsFromRemoteContext": true to your model JSON file.

Are only operation hooks affected by this issue ('before save', 'after save', etc), or are all remote methods subject to continuation passing issues? (i.e. would I have to update every place where I'm currently referencing req.accessToken?)

I think the code which can access accessToken directly via the incomingreq object should not need any changes. You can upgrade it to use options.accessToken instead of req.accessToken, but it shouldn't be required. I use "should" because it's hard to tell for me without seeing your code.

@antarasi
Copy link

Elegant way of getting userId from acccessToken by request object in remote method:

  MyModel.register = function(req, param, cb) {
    var userId = req.accessToken.userId;

    console.log(userId); 
    cb(null, ...);
  };

  MyModel.remoteMethod(
    'register',
    {
      accepts: [
        { arg: 'req', type: 'object', http: {source: 'req'} }, // <----
        { arg: 'param', type: 'string', required: true },
      ]
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests