Я хочу добавить функцию сброса / забытого пароля в свое личное приложение express.js. Я решил реализовать его аналогично тому, как это делает Django.
По сути, он генерирует уникальный токен на основе пользователя (идентификатор, хешированный пароль, адрес электронной почты, время последнего входа в систему и текущее время, все вместе с уникальным паролем и солью). Затем пользователь получает этот токен по ссылке для сброса пароля. Кто-то объяснил это лучше меня в одном из ответов stackoverflow.
А вот исходный код класса Django PasswordResetTokenGenerator
а>
Я размещу свою реализацию javascript внизу. Было бы неплохо, если бы вы проверили его на возможные недостатки, но это не мой главный вопрос :)
Итак, пользователь получает электронное письмо со ссылкой для сброса пароля. Ссылка выглядит так https://example.com/reset-password/MQ/58ix7l-35858854f74c35d0c64a5a17bd127f71cd3ad1da
, где:
MQ
- это идентификатор пользователя в кодировке base64 (в этом примере - 1)58ix7l
- метка времени в кодировке base3635858...
- фактический токен
Пользователь нажимает на ссылку. Сервер получает запрос GET - ›сервер проверяет, существует ли пользователь с таким идентификатором -› затем сервер проверяет правильность токена. Если все в порядке, сервер отправляет пользователю html-ответ с формой установки нового пароля.
До сих пор все было почти так же, как это делает django (с небольшими отличиями). Но теперь я хочу поступить иначе. Django (после получения запроса GET) устанавливает анонимный сеанс, сохраняет токен в сеансе и перенаправляет (302) для сброса формы пароля. На стороне клиента нет никаких признаков токена. Пользователь заполняет форму, POST-запрос отправляется на сервер с новым паролем. Сервер снова проверяет токен (хранящийся в сеансе). Если все верно - пароль изменен.
По какой-то причине (это сделает мое приложение намного сложнее :)) я не хочу добавлять анонимный сеанс, я не хочу хранить токен в сеансе.
Я хочу просто взять токен из req.params
- ›избежать его -› проверить, действителен ли он - ›и отправить пользователю с формой, например:
<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>
Пользователь отправляет форму, сервер снова проверяет токен, а затем меняет пароль.
Итак, после стены текста мой вопрос:
Насколько безопасно хранить токен в такой HTML-форме?
Я могу придумать одну возможную угрозу: злой пользователь может отправить кому-нибудь ссылку с <script>alert('boo!')</script>
вместо токена. Но это не должно быть проблемой, если токен был проверен и экранирован раньше. Какие-нибудь другие возможные дыры?
Как я уже сказал, я публикую свою реализацию javascript generateToken
, checkToken
, на всякий случай ...
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;
Спасибо
HttpOnly
cookie, к которому у javascript нет доступа. - person Taplar   schedule 14.02.2020