Как осуществить сброс пароля? - PullRequest
81 голосов
/ 20 марта 2009

Я работаю над приложением в ASP.NET, и мне было интересно, каким образом я мог бы реализовать функцию Password Reset, если бы я хотел свернуть свою собственную.

В частности, у меня есть следующие вопросы:

  • Каков хороший способ создания уникального идентификатора, который трудно взломать?
  • Должен ли к нему быть прикреплен таймер? Если так, как долго это должно быть?
  • Должен ли я записывать IP-адрес? Это вообще имеет значение?
  • Какую информацию мне следует запрашивать на экране «Сброс пароля»? Просто адрес электронной почты? Или, может быть, адрес электронной почты плюс некоторая информация, которую они «знают»? (Любимая команда, имя щенка и т. Д.)

Есть ли еще какие-то соображения, о которых мне нужно знать?

NB : Другие вопросы полностью затушеваны технической реализации. Действительно, принятый ответ омрачает кровавые детали. Я надеюсь, что этот вопрос и последующие ответы пойдут в мрачные подробности, и я надеюсь, сформулировав этот вопрос гораздо более узко, чтобы ответы были менее «пушистыми» и более «кровавыми».

Редактировать : Ответы, которые также касаются того, как такая таблица будет моделироваться и обрабатываться в SQL Server или любых ссылках ASP.NET MVC на ответ, приветствуются.

Ответы [ 7 ]

67 голосов
/ 31 марта 2009

РЕДАКТИРОВАТЬ 2012/05/22: как продолжение этого популярного ответа, я больше не использую GUID самостоятельно в этой процедуре. Как и другой популярный ответ, теперь я использую свой собственный алгоритм хеширования для генерации ключа для отправки в URL. Это имеет преимущество в том, что короче. Посмотрите на System.Security.Cryptography, чтобы сгенерировать их, в которых я обычно использую и соль.

Во-первых, не следует немедленно сбрасывать пароль пользователя.

Во-первых, не следует сразу же сбрасывать пароль пользователя при его запросе. Это нарушение безопасности, так как кто-то может угадать адреса электронной почты (т. Е. Ваш адрес электронной почты в компании) и сбрасывать пароли по своему усмотрению. В наши дни лучшие практики обычно включают ссылку «подтверждение», отправляемую на адрес электронной почты пользователя, подтверждающую, что он хочет сбросить ее. Эта ссылка, куда вы хотите отправить ссылку уникального ключа. Я отправляю мой со ссылкой, как: domain.com/User/PasswordReset/xjdk2ms92

Да, установите тайм-аут в ссылке и сохраните ключ и тайм-аут на своем бэкэнде (и соль, если вы его используете). Тайм-ауты в 3 дня являются нормой, и обязательно уведомите пользователя о 3 днях на веб-уровне, когда он запросит сброс.

Используйте уникальный хэш-ключ

Мой предыдущий ответ сказал использовать GUID. Сейчас я редактирую это, чтобы посоветовать всем использовать случайно сгенерированный хеш, например используя RNGCryptoServiceProvider. И убедитесь, что исключили любые "настоящие слова" из хеша. Я вспоминаю специальный телефонный звонок в 6 утра, когда женщина получила определенное слово "c" в своем хэш-ключе "предположим, что это случайный", что сделал разработчик. Doh!

Вся процедура

  • Пользователь нажимает «сбросить» пароль.
  • У пользователя запрашивается электронное письмо.
  • Пользователь вводит электронную почту и нажимает кнопку отправить. Не подтверждайте и не отклоняйте электронное письмо, так как это плохая практика. Просто скажите: «Мы отправили запрос на сброс пароля, если электронная почта подтверждена». или что-то похожее на загадку.
  • Вы создаете хеш из RNGCryptoServiceProvider, сохраняете его как отдельную сущность в таблице ut_UserPasswordRequests и ссылаетесь на пользователя. Это позволяет отслеживать старые запросы и информировать пользователя об истечении срока действия старых ссылок.
  • Отправьте ссылку на электронную почту.

Пользователь получает ссылку, например http://domain.com/User/PasswordReset/xjdk2ms92, и щелкает ее.

Если ссылка подтверждена, вы запрашиваете новый пароль. Все просто, и пользователь получает возможность установить собственный пароль. Или, установите здесь свой зашифрованный пароль и сообщите им свой новый пароль (и отправьте его по электронной почте).

66 голосов
/ 03 апреля 2009

Здесь много хороших ответов, я не буду повторять все это ...

За исключением одного вопроса, который повторяется здесь почти на каждый ответ, хотя он и неправильный:

Направляющие (реально) уникальны и статистически невозможно угадать.

Это не так, GUID являются очень слабыми идентификаторами и должны НЕ использоваться для предоставления доступа к учетной записи пользователя.
Если вы исследуете структуру, вы получите не более 128 бит ... что в наше время не так уж и много.
Из которых первая половина является типичным инвариантом (для генерирующей системы), а половина того, что осталось, зависит от времени (или что-то еще подобное).
В целом, это очень слабый и легко поддающийся механизму механизм.

Так что не используйте это!

Вместо этого просто используйте криптографически сильный генератор случайных чисел (System.Security.Cryptography.RNGCryptoServiceProvider) и получите не менее 256 бит необработанной энтропии.

Все остальные, как и многие другие ответы.

8 голосов
/ 30 марта 2009

Сначала нам нужно узнать, что вы уже знаете о пользователе. Очевидно, у вас есть имя пользователя и старый пароль. Что еще ты знаешь? У вас есть адрес электронной почты? У вас есть данные о любимом цветке пользователя?

Предполагая, что у вас есть имя пользователя, пароль и рабочий адрес электронной почты, вам необходимо добавить два поля в вашу таблицу пользователей (предположим, что это таблица базы данных): дата с именем new_passwd_expire и строка new_passwd_id.

Если у вас есть адрес электронной почты пользователя, когда кто-то запрашивает сброс пароля, вы обновляете таблицу пользователей следующим образом:

new_passwd_expire = now() + some number of days
new_passwd_id = some random string of characters (see below)

Далее вы отправляете электронное письмо пользователю по этому адресу:

Дорогой такой-то

Кто-то запросил новый пароль для учетной записи пользователя на <имя вашего сайта>. Если вы запрашивали сброс пароля, перейдите по этой ссылке:

http://example.com/yourscript.lang?update=>

Если эта ссылка не работает, вы можете перейти к http://example.com/yourscript.lang и ввести в форму следующее:

Если вы не запрашивали сброс пароля, вы можете проигнорировать это письмо.

Спасибо, Яда Яда

Теперь кодируем yourscript.lang: для этого скрипта нужна форма. Если обновление var передается по URL, форма просто запрашивает имя пользователя и адрес электронной почты. Если обновление не прошло, он запрашивает имя пользователя, адрес электронной почты и идентификационный код, отправленный в электронном письме. Вы также просите новый пароль (дважды, конечно).

Чтобы проверить новый пароль пользователя, убедитесь, что имя пользователя, адрес электронной почты и идентификационный код совпадают, что срок действия запроса не истек, а два новых пароля совпадают. В случае успеха вы меняете пароль пользователя на новый и очищаете поля сброса пароля из пользовательской таблицы. Также не забудьте выйти из системы / удалить все файлы cookie, связанные с входом, и перенаправить пользователя на страницу входа.

По сути, поле new_passwd_id является паролем, который работает только на странице сброса пароля.

Одно потенциальное улучшение: вы можете удалить из электронного письма. «Кто-то запросил сброс пароля для учетной записи на этом адресе электронной почты ...» Таким образом, делая имя пользователя чем-то, что знает только пользователь, перехватывается ли электронная почта. Я не начинал таким образом, потому что, если кто-то атакует аккаунт, он уже знает имя пользователя. Эта добавленная неясность останавливает атаку «человек посередине» в случае, если кто-то злоумышленник перехватит электронную почту.

Что касается ваших вопросов:

генерация случайной строки: она не должна быть чрезвычайно случайной. Достаточно любого генератора GUID или даже md5 (concat (salt, current_timestamp ())), где salt - это что-то в записи пользователя, например, была создана учетная запись timestamp. Это должно быть что-то, что пользователь не может видеть.

таймер: Да, это нужно просто для того, чтобы ваша база данных была в здравом уме. Требуется не больше недели, но как минимум 2 дня, поскольку вы никогда не знаете, как долго может длиться задержка электронной почты.

IP-адрес: поскольку электронная почта может быть отложена на несколько дней, IP-адрес полезен только для регистрации, а не для проверки. Если вы хотите войти, сделайте это, иначе вам это не нужно.

Экран сброса: см. Выше.

Надеюсь, что это покрывает. Удачи.

3 голосов
/ 20 марта 2009

GUID, отправляемый на адрес электронной почты записи, вероятно, достаточно для большинства обычных приложений - с тайм-аутом еще лучше.

В конце концов, если почтовый ящик пользователя был скомпрометирован (т. Е. У хакера есть логин / пароль для адреса электронной почты), вы ничего не можете с этим поделать.

2 голосов
/ 31 марта 2009

1) Для генерации уникального идентификатора вы можете использовать алгоритм Secure Hash. 2) таймер прилагается? Вы имели в виду истечение срока действия ссылки на сброс pwd? Да, вы можете иметь набор Expiry 3) Вы можете запросить дополнительную информацию, кроме emailId, для проверки. Как дата рождения или некоторые секретные вопросы 4) Вы также можете генерировать случайные символы и попросить ввести это также вместе с запрос .. чтобы убедиться, что запрос пароля не автоматизирован какими-либо шпионскими программами или подобными вещами ..

2 голосов
/ 20 марта 2009

Вы можете отправить электронное письмо пользователю со ссылкой. Эта ссылка будет содержать некоторую трудно угадываемую строку (например, GUID). На стороне сервера вы также сохраняете ту же строку, что и отправленную пользователю. Теперь, когда пользователь нажимает на ссылку, вы можете найти в своей записи БД с той же секретной строкой и сбросить ее пароль.

0 голосов
/ 17 сентября 2018

Я думаю, что руководство Microsoft для ASP.NET Identity - хорошее начало.

https://docs.microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity

Код, который я использую для удостоверения ASP.NET:

Web.Config:

<add key="AllowedHosts" value="example.com,example2.com" />

AccountController.cs:

[Route("RequestResetPasswordToken/{email}/")]
[HttpGet]
[AllowAnonymous]
public async Task<IHttpActionResult> GetResetPasswordToken([FromUri]string email)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    var user = await UserManager.FindByEmailAsync(email);
    if (user == null)
    {
        Logger.Warn("Password reset token requested for non existing email");
        // Don't reveal that the user does not exist
        return NoContent();
    }

    //Prevent Host Header Attack -> Password Reset Poisoning. 
    //If the IIS has a binding to accept connections on 80/443 the host parameter can be changed.
    //See https://security.stackexchange.com/a/170759/67046
    if (!ConfigurationManager.AppSettings["AllowedHosts"].Split(',').Contains(Request.RequestUri.Host)) {
            Logger.Warn($"Non allowed host detected for password reset {Request.RequestUri.Scheme}://{Request.Headers.Host}");
            return BadRequest();
    }

    Logger.Info("Creating password reset token for user id {0}", user.Id);

    var host = $"{Request.RequestUri.Scheme}://{Request.Headers.Host}";
    var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
    var callbackUrl = $"{host}/resetPassword/{HttpContext.Current.Server.UrlEncode(user.Email)}/{HttpContext.Current.Server.UrlEncode(token)}";

    var subject = "Client - Password reset.";
    var body = "<html><body>" +
               "<h2>Password reset</h2>" +
               $"<p>Hi {user.FullName}, <a href=\"{callbackUrl}\"> please click this link to reset your password </a></p>" +
               "</body></html>";

    var message = new IdentityMessage
    {
        Body = body,
        Destination = user.Email,
        Subject = subject
    };

    await UserManager.EmailService.SendAsync(message);

    return NoContent();
}

[HttpPost]
[Route("ResetPassword/")]
[AllowAnonymous]
public async Task<IHttpActionResult> ResetPasswordAsync(ResetPasswordRequestModel model)
{
    if (!ModelState.IsValid)
        return NoContent();

    var user = await UserManager.FindByEmailAsync(model.Email);
    if (user == null)
    {
        Logger.Warn("Reset password request for non existing email");
        return NoContent();
    }            

    if (!await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", model.Token, UserManager, user))
    {
        Logger.Warn("Reset password requested with wrong token");
        return NoContent();
    }

    var result = await UserManager.ResetPasswordAsync(user.Id, model.Token, model.NewPassword);

    if (result.Succeeded)
    {
        Logger.Info("Creating password reset token for user id {0}", user.Id);

        const string subject = "Client - Password reset success.";
        var body = "<html><body>" +
                   "<h1>Your password for Client was reset</h1>" +
                   $"<p>Hi {user.FullName}!</p>" +
                   "<p>Your password for Client was reset. Please inform us if you did not request this change.</p>" +
                   "</body></html>";

        var message = new IdentityMessage
        {
            Body = body,
            Destination = user.Email,
            Subject = subject
        };

        await UserManager.EmailService.SendAsync(message);
    }

    return NoContent();
}

public class ResetPasswordRequestModel
{
    [Required]
    [Display(Name = "Token")]
    public string Token { get; set; }

    [Required]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 10)]
    [DataType(DataType.Password)]
    [Display(Name = "New password")]
    public string NewPassword { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm new password")]
    [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...