使用访问令牌重置环回密码

时间:2022-11-15 13:14:02

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:

我正在开发一个使用Loopback作为框架的项目,包括用户和身份验证。我添加了一个密码重置路由生成并通过电子邮件发送,一切似乎都正常工作。最近,我发现密码重置似乎不起作用。此处重置密码的过程是:

  • Call password reset method for user
  • 为用户调用密码重置方法
  • Send email from reset event, including user ID and access token
  • 从重置事件发送电子邮件,包括用户ID和访问令牌
  • From reset link, set $http.defaults.headers.common.authorization to the passed token
  • 从重置链接,将$ http.defaults.headers.common.authorization设置为传递的令牌
  • Call user.prototype$updateAttributes (generated by lb-ng) to update password attribute based on a form
  • 调用user.prototype $ updateAttributes(由lb-ng生成)以根据表单更新密码属性

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.

预期的行为是密码将在密码重置表单上更新。相反,我得到一个授权错误,如401或500(似乎来回)。我注意到在发送给API的实际标头中,授权令牌与我从路由传递的内容不匹配。尝试使用LoopBackAUth.setUser设置它不起作用,也没有在实际发送请求之前不更新授权属性。

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.

当我第一次添加时,我肯定花时间测试它,我无法弄清楚会有什么改变来打破这个。我一直在关注loopback-faq-user-management中的示例,但是在该示例中我们有一个Angular前端而不是服务器端视图。

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.

我尝试完全打开ACL,看看我是否可以更新我的用户对象的密码(或任何属性)(从User继承,但它是自己的类型)。在尝试这样做时,我仍然得到401。

Edit #2:

编辑#2:

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

这是我的ACL和示例代码,我是如何调用它的。

ACLs from model definition

来自模型定义的ACL

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

auth.js

auth.js

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

4 个解决方案

#1


4  

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!

找出问题所在。在我们的应用程序服务器中,我们没有使用Loopback的令牌中间件。添加app.use(loopback.token());在启动服务器之前,重置链接中提供的访问令牌按预期工作!

#2


2  

@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.

@OverlappingElvis让我走上正轨。对于遇到此问题的其他人来说,这是一个更完整的答案。环回文档在这方面非常有限。

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

确保您在电子邮件中同时获得用户ID和令牌,并在表单中填充这些用户ID和令牌。

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;

}

#3


2  

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.

虽然上述所有答案都会有所帮助,但请注意,Loopback在验证时会在证明无效时销毁令牌。令牌将消失。因此,当您正在通过401的解决方案时,请确保每次尝试新的代码迭代时都要创建一个新的密码重置令牌。

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.

否则,您可能会发现自己正在寻找完全健康的代码来更改密码,但是在代码的上一次迭代中已经删除了一个令牌,导致您在看到另一个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.

在我的特定情况下,访问令牌存储在SQL Server数据库中,并且由于引入了时区问题,令牌将始终立即过期,因为我将options.useUTC设置为false。这导致所有新令牌在过去7200秒,超过密码重置令牌有效的900秒。我没注意到那些令牌被立即销毁并得出结论我的代码仍然存在问题,因为我看到了401的回报。事实上,401是由使用已经在服务器上消失的令牌引起的。

#4


1  

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.

1)我在服务器端公开新方法,从令牌更新密码。

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);

#1


4  

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!

找出问题所在。在我们的应用程序服务器中,我们没有使用Loopback的令牌中间件。添加app.use(loopback.token());在启动服务器之前,重置链接中提供的访问令牌按预期工作!

#2


2  

@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.

@OverlappingElvis让我走上正轨。对于遇到此问题的其他人来说,这是一个更完整的答案。环回文档在这方面非常有限。

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

确保您在电子邮件中同时获得用户ID和令牌,并在表单中填充这些用户ID和令牌。

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;

}

#3


2  

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.

虽然上述所有答案都会有所帮助,但请注意,Loopback在验证时会在证明无效时销毁令牌。令牌将消失。因此,当您正在通过401的解决方案时,请确保每次尝试新的代码迭代时都要创建一个新的密码重置令牌。

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.

否则,您可能会发现自己正在寻找完全健康的代码来更改密码,但是在代码的上一次迭代中已经删除了一个令牌,导致您在看到另一个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.

在我的特定情况下,访问令牌存储在SQL Server数据库中,并且由于引入了时区问题,令牌将始终立即过期,因为我将options.useUTC设置为false。这导致所有新令牌在过去7200秒,超过密码重置令牌有效的900秒。我没注意到那些令牌被立即销毁并得出结论我的代码仍然存在问题,因为我看到了401的回报。事实上,401是由使用已经在服务器上消失的令牌引起的。

#4


1  

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.

1)我在服务器端公开新方法,从令牌更新密码。

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);