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

后端 未结 1 897
轮回少年
轮回少年 2021-02-11 09:06

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 thei

1条回答
  •  予麋鹿
    予麋鹿 (楼主)
    2021-02-11 09:54

    You're on the right lines

    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.

    Verifying your email

    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.

    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!

    Reset password

    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.

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

    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!

    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!

    Unsubscribing

    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.

    Some code examples

    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
    

    Why do I use RNGCryptoServiceProvider?

    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.

    Why do I Base64 encode it?

    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.

    Something to keep in mind

    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.

    Notes for others

    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

    0 讨论(0)
提交回复
热议问题