简体   繁体   中英

Reset Loopback Password with Access Token

I'm working on a project that uses Loopback as a framework, and includes users and authentication. I added a password reset route generated and sent in an email, and everything seemed to be working correctly. Recently, I discovered that the password reset does not appear to be working. The process for resetting the password here is:

  • Call password reset method for user
  • Send email from reset event, including user ID and access token
  • From reset link, set $http.defaults.headers.common.authorization to the passed token
  • Call user.prototype$updateAttributes (generated by lb-ng) to update password attribute based on a form

The expected behavior is that the password would be updated on the password reset form. Instead, I get an authorization error as either a 401 or a 500 (seems to go back and forth). I notice that in the actual headers sent to the API, the authorization token does not match what I'm passing from the route. Trying to set it using LoopBackAUth.setUser doesn't work, and neither doesn't updating the authorization property before actually sending the request.

I definitely spent time testing this when it was first added, and I can't figure out what would have changed to break this. I've been following the example from loopback-faq-user-management, but we have an Angular front-end instead of the server side views in that example.

Edit:

I tried opening up the ACLs completely to see if I could update the password (or any properties) of my user object (which inherits from User, but is its own type). I'm still getting a 401 when trying to do this.

Edit #2:

Here are my ACLs and sample code for how I'm calling this.

ACLs from model definition

...
{
    "accessType": "*",
    "principalType": "ROLE",
    "principalId": "$owner",
    "permission": "ALLOW"
},
{
    "accessType": "EXECUTE",
    "principalType": "ROLE",
    "principalId": "$owner",
    "permission": "ALLOW",
    "property": "updateAttributes"
}
...

auth.js

...
resetPassword: function(user) {
    return MyUser.prototype$updateAttributes(user, user).$promise;
}
...

Figured out what the issue was. In our app's server, we were not using Loopback's token middleware. Adding app.use(loopback.token()); before starting the server causes the access token provided in the reset link to work as expected!

@OverlappingElvis put me on the right track. Here's a more complete answer for others running into this. The loopback docs are quite limited in this area.

Make sure that you get both the user id and the token in your email and these get populated in the form.

From the form the following code does the job:

 function resetPassword(id, token, password) {
  $http.defaults.headers.common.authorization = token;

  return User
    .prototype$updateAttributes({id:id}, {
     password: password
   })
   .$promise;

}

While all of the above answers will prove to be helpful, be aware that Loopback destroys a token during validation when it proved it to be invalid . The token will be gone. So when you're working your way through a solution for the 401's, make sure you're creating a new password reset token each time you try a new iteration of your code.

Otherwise you might find yourself looking at perfectly healthy code to change a password, but with a token that's already deleted in a previous iteration of your code, leading you to the false conclusion that you need to work on your code when you see another 401.

In my particular case the access tokens are stored in a SQL Server database and the token would always be immediately expired due to a timezone problem that was introduced, because I had options.useUTC set to false. That cause all newly tokens to be 7200 seconds in the past which is more than the 900 seconds than the password reset tokens are valid. I failed to notice that those tokens were immediately destroyed and concluded I had still problems with my code as I saw 401's in return. Where in fact the 401 was caused by using a token that was already gone on the server.

This was way more complicated than it ought to be. Here is my full solution:

1) I expose new method on server side which does the password updating from token.

Member.updatePasswordFromToken = (accessToken, __, newPassword, cb) => {
  const buildError = (code, error) => {
    const err = new Error(error);
    err.statusCode = 400;
    err.code = code;
    return err;
  };

  if (!accessToken) {
    cb(buildError('INVALID_TOKEN', 'token is null'));
    return;
  }

  Member.findById(accessToken.userId, function (err, user) {
    if (err) {
      cb(buildError('INVALID_USER', err));
      return;
    };
    user.updateAttribute('password', newPassword, function (err, user) {
      if (err) {
        cb(buildError('INVALID_OPERATION', err));
        return;
      }

      // successful,
      // notify that everything is OK!
      cb(null, null);
    });
  });
}

and I also define the accessibility:

Member.remoteMethod('updatePasswordFromToken', {
  isStatic: true,
  accepts: [
    {
      arg: 'accessToken',
      type: 'object',
      http: function(ctx) {
        return ctx.req.accessToken;
      }
    },
    {arg: 'access_token', type: 'string', required: true, 'http': { source: 'query' }},
    {arg: 'newPassword', type: 'string', required: true},      
  ],
  http: {path: '/update-password-from-token', verb: 'post'},
  returns: {type: 'boolean', arg: 'passwordChanged'}
});

From the client-side, I just call it like this:

this.memberApi.updatePasswordFromToken(token, newPassword);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM