I want to add reset/forgot password functionality to my personal express.js app. I decided to implement it in similar way how Django does it.
Basically, it generates unique token based on user (id, hashed password, email, last login time and current time, all mixed with unqiue password and salt). Then, user receives that token in his "reset password link". Someone explained it better than me in one of stackoverflow answers.
And here is the source code of Django PasswordResetTokenGenerator
class
I will post my javascript implementation on the bottom. It would be nice if you check it for possible flaws, but it is not my main question :)
So, user gets e-mail with "reset password link". Link looks like this https://example.com/reset-password/MQ/58ix7l-35858854f74c35d0c64a5a17bd127f71cd3ad1da
, where:
MQ
is base64 encoded user id (1 in this example)58ix7l
is base36 encoded timestamp35858...
is actual token
User clicks on link. Server receives GET request -> server checks if user with that id exist -> then server checks correctness of the token. If everything is OK, server sends user html response with "set new password" form.
So far, everything was almost exactly the same how django does it (few minor differences). But now I want to do somewhat different. Django (after receiving GET request) establishes anonymous session, stores token in session, and redirect (302) to reset password form. There is no any sign of token on the client side. User fills a form, POST request is send to server with new password. Server checks token (stored in session) again. If everything is right - password is changed.
For some reason (it will make my app a lot more complicated :)), I don't want to add anonymous session, I don't want to store token in session.
I want to just take token from req.params
-> escape it -> check if it is valid -> and send to user with form, like this:
<form action="/reset-password" method="POST">
<label for="new-password">New password</label><input id="new-password" type="password" name="new-password" />
<label for="repeat-new-password">Repeat new password</label><input id="repeat-new-password" type="password" name="repeat-new-password" />
<input name="token" type="hidden" value="58ix7l-35858854f74c35d0c64a5a17bd127f71cd3ad1da">
<input type="submit" value="Set new password" />
</form>
User sends form, server checks token again, and then change password.
So after wall of text, my question is:
Is it safe to store token in html form like this?
I can think of one possible threat: Evil user can send someone link with <script>alert('boo!')</script>
instead of token. But it should not be problem if token is validated and escaped before. Any other possible holes?
As I said before, im posting my generateToken
, checkToken
javascript implementation, just in case...
generate-change-password-token.js
const { differenceInSeconds } = require('date-fns');
const makeTokenWithTimestamp = require('../crypto/make-token-with-timestamp');
function generateChangePasswordToken(user) {
const timestamp = differenceInSeconds(new Date(), new Date(2010, 1, 1));
const token = makeTokenWithTimestamp(user, timestamp);
return token;
}
module.exports = generateChangePasswordToken;
verify-change-password-token.js
const crypto = require('crypto');
const { differenceInSeconds } = require('date-fns');
const makeTokenWithTimestamp = require('../crypto/make-token-with-timestamp');
function verifyChangePasswordToken(user, token) {
const timestamp = parseInt(token.split('-')[0], 36);
const difference = differenceInSeconds(new Date(), new Date(2010, 1, 1)) - timestamp;
if (difference > 60 * 60 * 24) {
return false;
}
const newToken = makeTokenWithTimestamp(user, timestamp);
const valid = crypto.timingSafeEqual(Buffer.from(token), Buffer.from(newToken));
if (valid === true) {
return true;
}
return false;
}
module.exports = verifyChangePasswordToken;
make-token-with-timestamp.js
const crypto = require('crypto');
function saltedHmac(keySalt, value, secret) {
const hash = crypto.createHash('sha1').update(keySalt + secret).digest('hex');
const hmac = crypto.createHmac('sha1', hash).update(value).digest('hex');
return hmac;
}
function makeHashValue(user, timestamp) {
const { last_login: lastLogin, id, password } = user;
const loginTimestamp = lastLogin ? lastLogin.getTime() : '';
return String(id) + password + String(loginTimestamp) + String(timestamp);
}
function makeTokenWithTimestamp(user, timestamp) {
const timestamp36 = timestamp.toString(36);
const hashValue = makeHashValue(user, timestamp);
const keySalt = process.env.KEY_SALT;
const secret = process.env.SECRET_KEY;
if (!(keySalt && secret)) {
throw new Error('You need to set KEY_SALT and SECRET_KEY in env variables');
}
const hashString = saltedHmac(keySalt, hashValue, secret);
return `${timestamp36}-${hashString}`;
}
module.exports = makeTokenWithTimestamp;
Thx
HttpOnly
cookie, that javascript has no access to. – Taplar