从 UUID 或 HMAC/JWT/hash 生成一次性安全令牌? [英] Generating one-time-only security tokens from UUID or HMAC/JWT/hash?

查看:16
本文介绍了从 UUID 或 HMAC/JWT/hash 生成一次性安全令牌?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm build the backend for a web app. When a new user goes to the site and clicks the Sign Up button, they'll fill out a super simple form asking them for their username + password and they'll submit. This prompts the server to send a verification email to that email address. They'll then check their email, click a link (which verifies their email) and then be routed to the login page so they can sign in if they choose.

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

In order to verify their email, when the server generates the email it will need to create (and store) a verification token (likely a UUID) and attach it to this link in the email, so that the link looks something like:

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

vt=12345 是验证令牌"(也可能是 UUID).因此,用户单击此链接,我的 GET v1/users/verify 端点查看令牌,以某种方式确认其有效,并进行一些数据库更新以激活"用户.他们现在可以登录了.

Where vt=12345 is the "verification token" (again likely a UUID). So the user clicks this link and my GET v1/users/verify endpoint looks at the token, somehow confirms its valid, and makes some DB updates to "activate" the user. They can now log in.

当用户想要取消订阅接收电子邮件,或者当他们不记得自己的密码并需要恢复密码以便他们可以登录时的类似情况.

Similar scenarios for when a user wants to unsubscribe from receiving email, or when they can't remember their password and need to recover it so that they can log in.

用户想停止接收电子邮件但仍想使用该应用程序.他们在我们发送给他们的每周时事通讯中单击退订"链接.该链接需要包含某种类似的取消订阅令牌",与上面的验证令牌一样,生成并存储在服务器上,用于验证用户取消订阅电子邮件的请求.

User wants to stop receiving emails but still wants to use the app. They click an "Unsubscribe" link in a weekly newsletter we send them. This link needs to contain some kind of similar "unsubscribe token" that, like the verification token above, is generated + stored on the server, and is used to authenticate the user's request to unsubscribe from email.

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

Here the user has forgotten their password and needs to recover it. So at the login screen they click the "Forgot my password" link, and are presented with a form where they must fill out their email address. Server sends an email to that address. They check this email and it contains a link to a form where they can enter their new password. This link needs to contain a "reset password token" that -- like the verification token above -- is generated + stored on the server, and is used to authenticate the user's request to change their password.

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

So here we have three very similar problems to solve, all requiring the use of what I'm calling "one-time only (OTO) security tokens". These OTO tokens:

  • 必须在服务器端生成并持久化(可能保存到 security_tokens 表中)
  • 必须是可以附加到我们将从电子邮件内部公开的链接的内容
  • 必须只有一次有效:一旦他们点击它,令牌就被使用"并且不能重复使用

我想出的解决方案很简单……几乎太简单了.

The solution I came up was simple...almost too simple.

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

For the tokens I am just generating random UUIDs (36-char) and storing them to a security_tokens table that has the following fields:

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

当服务器创建它们时,它们是UNCLAIMED".当用户单击表内的链接时,他们被认领".后台工作作业将定期运行以清理任何 CLAIMED 令牌或删除任何过期"的 UNCLAIMED 令牌(基于其 generated_on 字段).该应用程序还将忽略之前已声明(并且尚未清理)的任何令牌.

When the server creates them they are "UNCLAIMED". When the user clicks a link inside the table they are "CLAIMED". A background worker job will run periodically to clean up any CLAIMED tokens or to delte any UNCLAIMED tokens that have "expired" (based on their generated_on fields). The app will also ignore any tokens that have been previously CLAIMED (and have just not yet been cleaned up).

认为这个解决方案可行,但我不是一个超级安全的人,我担心这种方法:

I think this solution would work, but I'm not a super security guy and I'm worried that this approach:

  1. 可能会使我的应用受到某种类型的攻击/利用;和
  2. 当其他解决方案可能同样有效时,可能会重新发明轮子

就像上面的第二个一样,我想知道我是否应该使用与哈希/HMAC/JWT 相关的机制而不是死的简单 UUID.也许有一些聪明的加密/安全人员找到了一种方法,使这些令牌本身以安全/不可变的方式包含 CLAIM 状态和到期日期等.

Like for the 2nd one above I'm wondering if I should be using a hash/HMAC/JWT-related mechanism instead of a dead simple UUID. Maybe there's some smart crypto/security folks who found a way to make these tokens contain CLAIM status and expiration date themselves in a secure/immutable fashion, etc.

推荐答案

你是对的

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

I have a very similar method in my application based on what I want it to do. I have a table containing each user (a Users table) which I can use to reference each individual account and perform actions based on their identity. There are a lot of security threats to mitigate by adding in user accounts and self-management options. Here's how I combat a few of these vulnerabilities.

验证您的电子邮件

当用户注册时,服务器应该使用 RNGCryptoServiceProvider() 类来生成一个长度足够长的随机盐,它永远不会被实际猜到.然后,我对盐进行散列(单独)并对它应用 base64 编码,以便可以将其添加到 Url.通过电子邮件将完成的链接发送给用户,并确保将该哈希与相关 UserId 存储在 Users 表中.

When a user signs up, the server should use the RNGCryptoServiceProvider() class to generate a random salt with sufficient length that it could never realistically be guessed. Then, I hash the salt (on it's own) and apply base64 encoding to it so that it can be added to a Url. Send the completed link to the user via email, and be sure to store that hash against the relevant UserId in the Users table.

用户在收件箱中看到一个漂亮而简洁的单击此处验证您的电子邮件地址",然后可以单击该链接.它应该重定向到一个接受可选 url 参数的页面(例如 mywebsite.com/account/verifyemail/myhash 然后检查服务器端的哈希值.然后站点可以检查哈希值与激活已存储在数据库中的哈希值.如果匹配记录,则应将 Users.EmailVerified 列标记为 true 并提交到表中.然后,您可以删除该表中的验证记录条目.

The user sees a nice and neat "Click here to validate your email address" in their inbox and can click on the link. It should redirect to a page that accepts an optional url parameter (such as mywebsite.com/account/verifyemail/myhash and then check the hash server-side. The site can then check the hash against the activation hashes it has stored in the database. If it matches a record, then you should mark the Users.EmailVerified column to true and commit to the table. Then, you can delete that Verification record entry from the table.

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

Well done, you've successfully verified a user's email address is real!

重设密码

在这里,我们实现了一个类似的方法.但是,我们最好将记录存储在 PasswordResetRequest 表中,而不是验证记录,并且不要删除记录 - 这可以让您查看密码是否被重置以及何时重置.每次用户请求重置密码时,您应该显示一条匿名消息,例如一封电子邮件已发送到您的主电子邮件地址,其中包含进一步的说明".即使没有发送或帐户不存在,它也会阻止潜在的攻击者枚举用户名或电子邮件地址以查看它们是否已在您的服务中注册.同样,如果它们是真实的,请使用与以前相同的方法发送链接.

Here, we implement a similar method. But instead of a Verification record, we're better off storing our record in a PasswordResetRequest table, and do not delete records - this allows you to see whether or not a password was reset and when. Each time the user requests a password reset, you should display an anonymous message such as "An email was sent to your primary email address containing further instructions". Even if one was not sent or the account doesn't exist, it stops a potential attacker from enumerating usernames or email addresses to see if they are registered with your service. Again, if they are real, send a link using the same method as before.

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

The users opens their email address and clicks on the link. They are then redirected to a reset page such as mywebsite.com/account/resetpassword/myhash. The server then runs the hash in the url against the database and returns a result if it is real. Now, this is the tricky part - you shouldn't keep these active for long. I'd recommend a column linking the hash to the Users.UserId, one called ExpiraryDateTime which contains something like Datetime.Now.AddMinutes(15) (which makes it easier to work with later), and one called IsUsed as a boolean (false by default).

点击链接时,您应该检查链接是否存在.如果没有,请给他们默认的那个链接有问题.请请求一个新的"文本.但是,如果链接有效,您应该检查 Used == false 因为您不希望人们多次使用同一个链接.如果不使用,那就太好了!让我们检查一下它是否仍然有效.最简单的方法是一个简单的 if (PasswordResetRequest.ExpiraryDateTime < DateTime.Now) - 如果链接仍然有效,那么您可以继续进行密码重置.如果不是,则表示它是在不久前生成的,您不应该再使用它.说真的,有些网站今天仍然允许您生成链接,如果您的电子邮件在 1 个月后被黑客入侵,您仍然可以使用重置链接!

On clicking a link, you should check to see if a link exists. If not, give them them to the default "There was a problem with that link. Please request a new one" text. However, if the link is valid, you should check that Used == false because you don't want people using the same link more than once. If it's not used, great! Let's check to see if it's still valid. The easiest way would be a simple if (PasswordResetRequest.ExpiraryDateTime < DateTime.Now) - if the link is still valid, then you can proceed with the password reset. If not, it means it was generated a while ago and you shouldn't allow it to be used anymore. Seriously, some sites will still allow you to generate a link today and if your email is hacked 1 month from now, you can still use the reset links!

我还应该提到,每次用户请求重置密码时,您应该检查表中的现有记录以获取有效链接.如果一个有效(意味着它仍然可以使用),那么您应该立即将其无效.将哈希替换为一些辅助文本,例如无效:用户请求新的重置链接".这也让您知道他们已经请求了多个链接,同时也使他们的链接无效.您也可以将其标记为已使用",如果您真的想通过聪明并将整个无效:用户请求的新重置链接"作为编码 URL 偷偷进入他们的浏览器来防止人们尝试使用过期链接.同一个帐户不应该有多个处于活动状态的重置链接 - 永远

I should also mention that each time the user requests a password reset, you should check the existing records in the table for a valid link. If one is valid (meaning it can still be used) then you should instantly invalidate it. Replace the hash with some assistive text like "Invalid: User requested new reset link". This also lets you know they've requested more than one link whilst also invalidating their link. You could also mark it as Used if you really wanted to just to prevent people from trying to use expired links by being smart and sneaking the whole "Invalid: User requested new reset link" as an encoded URL into their browsers. You should never have more than one reset link active for the same account - ever!

退订

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

For this, I'd have a simple flag in the database that determines whether or not a user can receive promotional offers and newsletters etc. So a Users.SubscribedToNewsletter would suffice. They should be able to log in and change this in their Email Settings or Communication Preferences etc.

一些代码示例

这是我在 C# 中的 RNGCryptoServiceProvider 代码

This is my RNGCryptoServiceProvider code in C#

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# 类)允许您根据完全随机且不可重现的事件生成看似随机的字节字符串.Random() 之类的类仍然需要使用某种内部数据来根据可预测的算法事件(例如当前日期和时间)生成一个数字.RNGCryptoServiceProvider() 使用诸如 cpu 温度、正在运行的进程数量等所有内容来创建无法复制的随机内容.这允许最终的字节数组尽可能随机.

The RNGCryptoServiceProvider() (which is a C# class in their Security library) allows you to generate a seemingly random string of bytes based on entirely random and non-reproducable events. Classes like the Random() still need to use some sort of internal data to generate a number based on predictable algorithmic events such as current date and time. The RNGCryptoServiceProvider() uses things like cpu temperatures, number of running processes, etc. all to create something random that can't be reproduced. This allows for the final byte array to be as random as possible.

我为什么要 Base64 编码?

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

Base64 encoding will result in a string containing only numbers and letters. This means there will be no symbols or encoded characters within the text and therefore it is safe to use in a URL. This isn't so much a security feature, but it does allow you to only allow numbers and letters within the parameters of the method, and filter out or reject any input that doesn't match this standard. For example, filtering out any inputs that contain the chevrons < and > should allow you to prevent XSS.

注意事项

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

You should ALWAYS assume that the link containing your hash is invalid until you perform each check on it to ensure it passes requirements. So you can do your various if statements but unless you pass every single one, you leave your default next action to some form of error for the user. To clarify, I should check that the password reset link is valid, then not used, then still within the time window, and then perform my reset actions. Should it fail to pass any of these requirements, the default action should be to give the user an error saying that it is an invalid link.

给他人的备注

由于我非常有信心这不是唯一实现此目的的方法,我只想声明这是我多年来一直这样做的方式,从未让我失望过并让我的公司通过了几次广泛的渗透测试.但是,如果有人有更好/更安全的方法,请做一些说明,因为我很乐意了解更多信息.如果您对我提到的特定部分有任何其他问题或需要澄清,请告诉我,我会尽力提供帮助

Since I'm pretty confident this isn't the only way to do this, I'd just like to declare that this is how I've done it for years which has never failed me and has gotten my company through several extensive pentests. But if someone has a better / more secure way of doing so, please do shed some light as I'd be happy to learn more. If you have any further questions or need clarification on a particular part I mentioned, just let me know and I'll try my best to help

这篇关于从 UUID 或 HMAC/JWT/hash 生成一次性安全令牌?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆