Как реализовать сброс пароля?


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

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

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

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

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

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

7 80

7 ответов:

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

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

GUID (реалистично) уникальны и статистически невозможно угадать.

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

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

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

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

EDIT 2012/05/22: в качестве продолжения этого популярного ответа я больше не использую GUID в этой процедуре. Как и другой популярный ответ, теперь я использую свой собственный алгоритм хэширования для генерации ключа для отправки URL-адреса. Это имеет то преимущество, что он также короче. Загляните в систему.Безопасность.Криптография для их генерации, которую я обычно использую соль.

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

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

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

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

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

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

  • пользователь нажимает кнопку "Сброс" пароля.
  • пользователь по электронной почте.
  • пользователь вводит электронную почту и нажимает отправить. Не подтверждайте и не отрицайте электронную почту, так как это тоже плохая практика. Просто скажите: "мы отправили запрос на сброс пароля, если электронная почта проверена.- или что-то загадочное.
  • вы создаете хэш из RNGCryptoServiceProvider, хранить его как отдельная сущность в ut_UserPasswordRequests таблица и ссылка обратно к пользователю. Таким образом, вы можете отслеживать старые запросы и сообщать пользователю, что старые ссылки истекли.
  • отправить ссылку на электронную почту.

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

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

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

предполагая, что у вас есть имя пользователя, пароль и рабочий адрес электронной почты, вам нужно добавить два поля в таблицу пользователей (предполагая, что это таблица базы данных): дату с именем 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.этот скрипт нуждается в форме. Если обновление var передано по URL-адресу, форма просто запрашивает имя пользователя и адрес электронной почты. Если обновление не прошло, он запрашивает имя пользователя, адрес электронной почты и идентификационный код, отправленный по электронной почте. Вы также попросите новый пароль (дважды, конечно).

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

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

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

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

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

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

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

сброс экрана: см. выше.

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

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

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

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

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

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

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; }
}