从 UUID 或 HMAC/JWT/hash 生成一次性安全令牌?

Generating one-time-only security tokens from UUID or HMAC/JWT/hash?

我正在为网络应用构建后端。当新用户访问该站点并单击 注册 按钮时,他们将填写一个超级简单的表格,要求他们提供用户名 + 密码,然后他们将提交。这会提示服务器向该电子邮件地址发送一封验证电子邮件。然后他们将检查他们的电子邮件,单击 link(验证他们的电子邮件),然后被路由到登录页面,以便他们可以选择登录。

为了验证他们的电子邮件,当服务器生成电子邮件时,它需要创建(并存储)一个验证令牌(可能是一个 UUID)并将其附加到此link 在电子邮件中,因此 link 看起来像:

"https://api.myapp.example.com/v1/users/verify?vt=12345"

其中 vt=12345 是 "verification token"(同样可能是 UUID)。因此,用户单击此 link,我的 GET v1/users/verify 端点查看令牌,以某种方式确认其有效,并对 "activate" 用户进行一些数据库更新。他们现在可以登录了。

当用户想要取消订阅接收电子邮件时,或者当他们忘记密码并需要恢复密码以便登录时的类似场景。

退订

用户想停止接收电子邮件但仍想使用该应用程序。他们在我们发送给他们的每周通讯中点击“取消订阅”link。这个link需要包含某种类似"unsubscribe token"的东西,就像上面的验证令牌一样,生成+存储在服务器上,用于验证用户退订电子邮件的请求。

找回密码

此处用户忘记了密码,需要找回密码。因此,在登录屏幕上,他们单击“忘记了我的密码”link,然后会看到一个表格,他们必须在其中填写电子邮件地址。服务器向该地址发送电子邮件。他们检查了这封电子邮件,其中包含一个 link 表单,他们可以在其中输入新密码。这个 link 需要包含一个 "reset password token" ,就像上面的验证令牌一样 - 生成+存储在服务器上,用于验证用户更改密码的请求。

所以这里我们要解决三个非常相似的问题,都需要使用我所说的“一次性 (OTO) 安全令牌”。这些 OTO 代币:

我的问题

我提出的解决方案很简单……几乎太简单了。

对于令牌,我只是生成随机 UUID(36 个字符)并将它们存储到具有以下字段的 security_tokens table:

[security_tokens]
---
id (PK)
user_id (FK to [users] table)
token (the token itself)
status (UNCLAIMED or CLAIMED)
generated_on (DATETIME when created)

当服务器创建它们时,它们是 "UNCLAIMED"。当用户单击 table 中的 link 时,它们是 "CLAIMED"。后台工作人员作业将 运行 定期清理任何 CLAIMED 令牌或删除任何具有 "expired" 的 UNCLAIMED 令牌(基于它们的 generated_on 字段)。该应用程序还将忽略之前已声明的任何令牌(并且尚未清理)。

认为这个解决方案可行,但我不是超级安全专家,我担心这个方法:

  1. 可能让我的应用对某种类型的 attack/exploit 开放;和
  2. 当其他解决方案可能同样有效时,可能会重新发明轮子

就像上面的第二个一样,我想知道我是否应该使用 hash/HMAC/JWT-related 机制而不是简单的 UUID。也许有一些聪明的 crypto/security 人找到了一种方法,使这些令牌本身以 secure/immutable 的方式包含 CLAIM 状态和到期日期等

你走对了

根据我的要求,我的应用程序中有一个非常相似的方法。我有一个 table 包含每个用户(一个用户 table),我可以用它来引用每个单独的帐户并根据他们的身份执行操作。通过添加用户帐户和自我管理选项,可以缓解 很多 的安全威胁。以下是我如何解决其中一些漏洞。

正在验证您的电子邮件

当用户注册时,服务器应使用 RNGCryptoServiceProvider() class 生成一个长度足够长的随机盐,使其永远无法被猜到。然后,我对盐进行哈希处理(单独)并对其应用 base64 编码,以便可以将其添加到 Url。通过电子邮件将完成的 link 发送给用户,并确保将该哈希值与相关 UserId 存储在 Users table.

用户在他们的收件箱中看到漂亮整洁的 "Click here to validate your email address",可以点击 link。它应该重定向到接受可选 url 参数(例如 mywebsite.com/account/verifyemail/myhash 的页面,然后检查哈希服务器端。然后该站点可以根据它存储在数据库中的激活哈希检查哈希. 如果它匹配一条记录,那么你应该将 Users.EmailVerified 列标记为 true 并提交到 table。然后,你可以从 table 中删除该验证记录条目.

干得好,您已成功验证用户的电子邮件地址是真实的!

重设密码

在这里,我们实现了一个类似的方法。但是,我们最好将记录存储在 PasswordResetRequest table 中,而不是验证记录,并且不要删除记录 - 这使您可以查看密码是否已重置以及何时重置。每次用户请求重设密码时,您应该显示一条匿名消息,例如 "An email was sent to your primary email address containing further instructions"。即使未发送或帐户不存在,它也会阻止潜在的攻击者枚举用户名或电子邮件地址以查看它们是否已在您的服务中注册。同样,如果它们是真实的,请使用与之前相同的方法发送 link。

用户打开他们的电子邮件地址并单击 link。然后他们将被重定向到重置页面,例如 mywebsite.com/account/resetpassword/myhash。然后,服务器针对数据库运行 url 中的散列,如果结果是真实的,则 returns 结果。现在,这是棘手的部分 - 你不应该长时间保持这些活动。我推荐一列 link 将散列到 Users.UserId,一个名为 ExpiraryDateTime 的列,其中包含类似 Datetime.Now.AddMinutes(15) 的内容(这使得以后更容易使用),并且一个称为 IsUsed 作为布尔值(默认为 false)。

单击 link 时,您应该检查 link 是否存在。如果没有,请将它们提供给默认的 "There was a problem with that link. Please request a new one" 文本。但是,如果 link 有效,您应该检查 Used == false,因为您不希望人们多次使用相同的 link。如果不使用,很好!让我们检查一下它是否仍然有效。最简单的方法是简单的 if (PasswordResetRequest.ExpiraryDateTime < DateTime.Now) - 如果 link 仍然有效,那么您可以继续进行密码重置。如果不是,则表示它是不久前生成的,您不应再允许使用它。说真的,有些网站今天仍然允许您生成 link,如果您的电子邮件在 1 个月后被黑,您仍然可以使用重置 links!

我还应该提一下,每次用户请求重设密码时,您应该检查 table 中的现有记录是否 有效 link .如果一个是有效的(意味着它仍然可以使用)那么你应该立即使它无效。将散列替换为一些辅助文本,例如 "Invalid: User requested new reset link"。这也让您知道他们已经请求了多个 link,同时还使他们的 link 无效。你也可以将其标记为已使用,如果你真的只是想通过聪明地将整个 "Invalid: User requested new reset link" 作为编码的 URL 潜入他们的浏览器来防止人们试图使用过期的 link .同一个帐户的重置 link 不应超过一个 - ever!

取消订阅

为此,我在数据库中有一个简单的标志,用于确定用户是否可以接收促销优惠和时事通讯等。因此 Users.SubscribedToNewsletter 就足够了。他们应该能够登录并在他们的电子邮件设置或通信首选项等中更改此设置。

一些代码示例

这是我在 C# 中的 RNGCryptoServiceProvider 代码

public static string GenerateRandomString(RNGCryptoServiceProvider rng, int size)
{
    var bytes = new Byte[size];

    rng.GetBytes(bytes);

    return Convert.ToBase64String(bytes);
}

var rng = new RNGCryptoServiceProvider();
var randomString = GenerateRandomSalt(rng, 47); // This will end up being a string of almost entirely random bytes

为什么要使用 RNGCryptoServiceProvider?

RNGCryptoServiceProvider()(这是他们的安全库中的 C# class)允许您根据完全随机且不可重现的事件生成看似随机的字节串。 类 与 Random() 一样,仍然需要使用某种内部数据来生成基于 predictable 算法事件(例如当前日期和时间)的数字。 RNGCryptoServiceProvider() 使用诸如 cpu 温度、运行 进程数等之类的东西来创建无法复制的随机东西。这允许最终字节数组尽可能随机。

为什么要Base64编码呢?

Base64 编码将生成仅包含数字和字母的字符串。这意味着文本中不会有符号或编码字符,因此在 URL 中使用是安全的。这算不上是安全功能,但它确实允许您在方法的参数中只允许数字和字母,并过滤掉或拒绝任何不符合该标准的输入。例如,过滤掉任何包含 V 形 <> 的输入应该可以防止 XSS。

注意事项

您应该始终假设包含您的散列的 link 是 无效的 ,直到您对其执行每项检查以确保它通过要求。因此,您可以执行各种 if 语句,但除非您传递 每一个 ,否则您将默认的 下一步操作 保留为某种形式用户的错误。澄清一下,我应该检查密码重置 link 是否有效,然后未使用,然后仍在 window、 时间内执行我的重置操作。如果它未能通过任何这些要求,默认操作应该是给用户一个错误,说明它是一个无效的 link.

其他人的注释

因为我非常有信心这不是 唯一的 方法,所以我只想声明这就是我多年来的做法它从未让我失望,并且让我的公司通过了几次广泛的渗透测试。但是,如果有人有更好/更安全的方法,请说明一下,因为我很乐意了解更多信息。如果您对我提到的特定部分有任何进一步的问题或需要澄清,请告诉我,我会尽力提供帮助