Please Request a Password Reset Again as the Last Reset Token Has Expired for Security Reasons
June 01st, 2021
Implementing a forgot password flow (with pseudo code)
Table of contents:
Security Issues to consider:
- Animal Strength Attacks
- Theft of password reset tokens from the database
- Reusing existing tokens
- Stealing tokens through email hijacking
How to implement a secure countersign reset catamenia
- User'southward enters electronic mail in the UI
- Creating countersign reset token and storing it in DB
- Sending token to the user and verifying it when used
What actually happens on the backend (pseudo code)
Applications need to account for the frequency with which users forget their passwords. This opens a potential attack vector because anyone can request a new countersign on behalf of the legitimate user. Resetting a password requires sending a token to a user'south electronic mail address and this provides an opening for attackers. Making sure you have a secure procedure for handling the password reset tokens will ensure your users' accounts remain safe from attackers.
Security Issues to Consider
Animate being forcefulness attacks
This is a common threat for all web applications. Attackers may attempt to detect patterns in the password reset tokens - like if it's derived from a user's userId, time they signed up, their email or any other information. Attackers may also endeavor all possible combinations of letters and numbers (brute forcefulness), and may fifty-fifty succeed if the generated tokens are not long or random plenty (i.due east. have low entropy).
To prevent this, we must ensure that tokens are generated using a secure random source, and that they are long plenty (we recommend >= 64 characters). Later on in this blog, we volition meet one such method.
Database theft of password reset tokens
There are several ways an aggressor can gain access to an application's database: SQL injection attacks, targeting unpatched database vulnerabilities, and exploiting unused database services. They could even gain access if someone hasn't updated the default login credentials.
While in that location are enough of issues with an attacker getting database access, ane of them is their ability to get users' countersign reset tokens, like in this research on Paleohacks. To mitigate this risk, we store but the hashed version of tokens in the database. Passwords are hashed and stored and it's important that password reset tokens are too (for the same reasons).
Some other related set on vector is the utilize of JWTs as the password reset token. Whilst this makes development easy, a major take chances is that if the secret key used to sign them is compromised, the attacker can use that to generate their own valid JWT. This would allot them to reset any user'southward password. The JWT surreptitious fundamental (or signing key) must exist carefully protected and hence we practice not recommend using JWTs as the password reset token.
Reusing existing tokens
For simplicity of development, it may be tempting to store a "static" password reset token per user. This token might exist randomly generated on user sign upwards, or based on their password'south hash or some other information.
This in turn implies that these tokens cannot be stored in a hashed course in the database - since we will need to send this token over email (because if we hash information technology, we tin can't unhash information technology at the time). Therefore, if the database is compromised, these tokens can be used to reset a user's password.
Another risk of reusing tokens is that if a token somehow gets leaked, even if it has been redeemed by the actual user, it can however be used by an assaulter to change that user's countersign.
Stealing tokens from e-mail hijacking
Attackers tin can gain admission to a user's email account through email hijacking. This usually happens with phishing emails, social engineering, or by getting them to enter their credentials in a artificial login form. Once they have access to a user's e-mail, they are able to trigger a password reset link and gain access to the user'due south account.
This risk can be mitigated by enabling two-cistron authentication via SMS or an authenticator app, or by request secret questions to the user before allowing them to reset their password.
How to implement a secure countersign reset flow
To ensure that your password reset process is equally secure as possible, hither is a potential flow that takes into account the security issues we discussed above.
User enters their e-mail in the UI requesting a password reset
This is how users volition begin the process for updating their password. There'due south usually a simple form available that lets them enter the email address associated with their account.
When they submit the e-mail address, this will trigger the dorsum-end to check if that email exists in the database. Fifty-fifty if the e-mail doesn't exist, nosotros'll testify a message that says the email has been sent successfully. That manner nosotros don't give attackers whatever indication that they should try a dissimilar e-mail address.
If the email does exist in the database, then we create a new password reset token, store its hashed version in the database, and generate a password reset link that's sent to the user'south email address.
Create a countersign reset token
With Supertokens, the password reset token is generated using a random 64 character string. This prevents animate being force attacks mentioned above since new tokens are unguessable, and have high entropy.
For Coffee (uptill line 120)
For Coffee (uptill line 120)
For NodeJS
For NodeJS
For Python and use secrets.token_urlsafe
For Python and use secrets.token_urlsafe
Store the token in the database
After the token has been created, information technology's hashed using SHA256 and stored in the database along with the user'south ID and it's assigned an expiration time. That mode the token is only valid for a set amount of time, blocking attacks that could happen if the token never expired. Here's an example of the db schema we can utilize to store password reset tokens.
CREATE Table password_reset_tokens (
user_id VARCHAR ( 36 ) Non Cypher ,
token VARCHAR ( 128 ) NOT Zilch UNIQUE ,
token_expiry BIGINT UNSIGNED Non Nothing,
Primary KEY ( user_id, token ),
);
If y'all observe, we let multiple tokens to be stored per user. This is necessary since we merely shop the hashed version of the tokens in the db. This means that if a user requests multiple tokens at the same time, we cannot send them the aforementioned previously generated token (which is not nevertheless redeemed), since it's stored in hashed form.
At the end, we want to generate a password reset link which points to a link on your website that displays the "enter new password" form, and likewise contains the token. An case of this is:
https://case.com/reset-password?token=<Token here>
The user clicks on the link in the e-mail
Once the user has received the email, they will click on the link and it volition redirect them to a page on the website to enter their new countersign. The countersign validators should follow the same rules equally that in a sign upwards grade.
The new password is submitted along with the token to the back-end
One time they enter the new password, the reset token and the new password are sent to the dorsum-end. The reset password token is obtained from the password reset link's query params.
In summary, if the token's hash matches what was stored in the database, the user's countersign will be updated with the new password. Otherwise, the user will have to asking a new reset token and go through the process once again.
What happens on the back-end
Here is a pseudo code of your backend API logic for redeeming a token:
office redeemToken ( passwordResetToken , newPassword ) {
/*
First we hash the token, and query the db based on the hashed value. If nil is found, then we throw an error.
*/
hashedToken = hash_sha256 ( passwordResetToken );
rowFromDb = db . getRowTheContains ( hashedToken )
if ( rowFromDb == nix ) {
throw Fault ( "invalid password reset token" )
}
userId = rowFromDb . user_id
/*
Now we know that the token exists, and then it is valid. Nosotros outset a transaction to preclude race weather.
*/
db_startTransaction () {
/* allTokensForUser is an array of db rows. Nosotros have to utilize a query that locks all the rows in the tabular array that belong to this userId. We tin use something like "SELECT * FROM password_reset_tokens where user_id = userId FOR UPDATE". The "FOR UPDATE" part locks all the relevant rows. */
allTokensForUser = db_getAllTokensBasedOnUser ( userId )
/*
We search for the row that matches the input token's hash, so that we know that another transaction has not redeemed it already.
*/
matchedRow = zero ;
allTokensForUser . forEach(row => {
if ( row . token == hashedToken ) {
matchedRow = row ;
}
});
if ( matchedRow == null ) {
/* The token was redeemed by another transaction. So nosotros exit
*/
throw Mistake ( " invalid password reset token " )
}
/*
At present we will delete all the tokens belonging to this user to foreclose duplicate use
*/
db_deleteAllRowsForUser ( userId )
/*
Now we cheque if the current token has expired or not.
*/
if ( matchedRow . token_expiry < time_now ()) {
db_rollback ();
throw Error ( " Token has expired. Please endeavor again " );
}
/*
At present all checks have been completed. We can alter the user's password
*/
hashedAndSaltedPassword = hashAndSaltPassword ( newPassword );
db_saveNewPassword ( userId, hashedAndSaltedPassword );
db_commitTransaction ();
}
}
function redeemToken ( passwordResetToken , newPassword ) {
/*
First we hash the token, and query the db based on the hashed value. If zilch is found, and so we throw an error.
*/
hashedToken = hash_sha256 ( passwordResetToken );
rowFromDb = db . getRowTheContains ( hashedToken )
if ( rowFromDb == goose egg ) {
throw Error ( "invalid countersign reset token" )
}
userId = rowFromDb . user_id
/*
Now we know that the token exists, and so it is valid. We start a transaction to prevent race conditions.
*/
db_startTransaction () {
/* allTokensForUser is an array of db rows. We accept to employ a query that locks all the rows in the table that belong to this userId. We can use something like "SELECT * FROM password_reset_tokens where user_id = userId FOR UPDATE". The "FOR UPDATE" part locks all the relevant rows.
*/
allTokensForUser =db_getAllTokensBasedOnUser ( userId )
/*
We search for the row that matches the input token'southward hash, and then that we know that another transaction has not redeemed information technology already.
*/
matchedRow = cipher ;
allTokensForUser . forEach(row => {
if ( row . token == hashedToken ) {
matchedRow = row ;
}
});
if ( matchedRow == null ) {
/* The token was redeemed past some other transaction. So we exit */
throw Error ( " invalid password reset token " )
}
/*
Now we will delete all the tokens belonging to this user to prevent indistinguishable employ
*/
db_deleteAllRowsForUser ( userId )
/*
Now nosotros bank check if the current token has expired or not.
*/
if ( matchedRow . token_expiry < time_now ()) {
db_rollback ();
throw Error ( " Token has expired. Please effort again " );
}
/* At present all checks have been completed. Nosotros can change the user's password */
hashedAndSaltedPassword = hashAndSaltPassword ( newPassword );
db_saveNewPassword ( userId,hashedAndSaltedPassword );
db_commitTransaction ();
}
}
function redeemToken ( passwordResetToken ,newPassword ) {
/*
Get-go we hash the token, and query the db based on the hashed value. If aught is found, and so we throw an error.
*/
hashedToken = hash_sha256 (
passwordResetToken );
rowFromDb = db . getRowTheContains (
hashedToken )
if ( rowFromDb == null ) {
throw Fault ( "invalid countersign reset token" )
}
userId = rowFromDb . user_id
/*
Now nosotros know that the token exists, and so information technology is valid. We first a transaction to prevent race conditions.
*/
db_startTransaction () {
/* allTokensForUser is an array of db rows. Nosotros accept to use a query
that locks all the rows in the tabular array that belong to this userId. Nosotros tin can apply something like "SELECT * FROM password_reset_tokens where user_id = userId FOR UPDATE". The "FOR UPDATE" part locks all the relevant rows. */
allTokensForUser = db_getAllTokensBasedOnUser ( userId )
/*
We search for the row that matches the input token'southward hash, and so that we know that another transaction has not redeemed information technology already. */
matchedRow = cypher ;
allTokensForUser . forEach(row => {
if ( row . token == hashedToken ) {
matchedRow = row ;
}
});
if ( matchedRow == nix ) {
/* The token was redeemed by another transaction. So we exit */
throw Error ( " invalid countersign reset token " )
}
/*
Now we will delete all the tokens belonging to this user to foreclose indistinguishable employ
*/
db_deleteAllRowsForUser ( userId )
/*
At present nosotros bank check if the current token has expired or not.
*/
if ( matchedRow . token_expiry < time_now ()) {
db_rollback ();
throw Error ( " Token has expired. Please endeavour once again " );
}
/* Now all checks have been completed. We can modify the user's password */
hashedAndSaltedPassword =( newPassword );
db_saveNewPassword ( userId, hashedAndSaltedPassword );
db_commitTransaction ();
}
}
Conclusion
Countersign reset flows are easy to become incorrect. One needs to be knowledgeable in cryptography, database transactions / distributed locking, and should be able to think virtually all the edge cases in the menstruation. The cost of getting these incorrect tin pb to compromised user accounts.
At supertokens.com, we have tried to provide an easy to use, open source, countersign reset solution (and lots of other auth modules) that you can apply to secure your app and save time.
Written by the Folks at SuperTokens — hope you lot enjoyed! We are always available on our Discord server. Join us if y'all have any questions or need any help.
tarkingtonsculd1974.blogspot.com
Source: https://supertokens.com/blog/implementing-a-forgot-password-flow
0 Response to "Please Request a Password Reset Again as the Last Reset Token Has Expired for Security Reasons"
Postar um comentário