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

81

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

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

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

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

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

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

Джордж Стокер
источник
ASP.NET MVC использует поставщика проверки подлинности ASP.NET по умолчанию, поэтому любые образцы кода, которые вы найдете в этом s, должны быть актуальны для ваших целей.
Paulwhit

Ответы:

66

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

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

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

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

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

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

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

AviD
источник
6
Абсолютно согласен, насколько мне известно, GUID никогда не создавались так, чтобы быть криптографически стойкими и их невозможно было угадать.
Ян Солтис,
5
хорошо сказано, AFAIK MSDN четко заявляет, что GUID не должен использоваться для безопасности.
доктор. Evil
2
UUID версии 4 используются в Windows с 2000 года: как генерируются идентификаторы GUID .NET 4? - Переполнение стека . В них есть 122 случайных бита, что, на мой взгляд, соответствует рекомендациям NIST. Была очень серьезная уязвимость для локальной атаки, которая, согласно CryptGenRandom - Wikipedia, была исправлена ​​в Vista и XP к 2008 году. Итак, где вы видите проблемы с текущим использованием GUID?
nealmcb
4
Этот блог "Old New Thing" описывает устаревшие UUID версии 1 и цитирует Internet Draft (то, что вы никогда не должны делать), срок действия которого истек в 1998 году, за 10 лет до публикации в блоге. Я бы скептически отнесся к ним в будущем. Мы вели эти битвы давным-давно и, кажется, выиграли большинство из них. Я все еще согласен с тем, что использование чистого API-вызова к крипто-случайному источнику намного лучше, но не так сложно с GUID / UUID в версии 4.
nealmcb
1
Как бы то ни было, это не дает ответа на вопрос «как сбросить пароль». Вас просто вырвало много замечаний по поводу GUID.
Рекс Уиттен
67

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

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

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

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

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

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

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

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

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

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

eduncan911
источник
1
Мне было интересно, если фактический пароль пользователя хешируется, зачем создавать новый HASH-ключ? Было бы неправильно отправлять электронное письмо пользователю со ссылкой для сброса пароля, передавая хешированный пароль? Хешированный пароль нельзя восстановить, когда пользователь нажимает на ссылку, сервер получит хешированный пароль, сравнит его с фактическим сохраненным, а затем позволит пользователю изменить пароль.
Даниэль
И еще одна приятная вещь в этом заключается в том, что вам не нужно устанавливать тайм-аут, как только пользователь изменил пароль, старая ссылка автоматически больше не будет действительна, потому что хешированный пароль, хранящийся в базе данных, был изменен.
Даниэль
@ Дэниел, это действительно плохая идея. Думаю, вам нужно погуглить термин "атаки методом перебора". Кроме того, причина, по которой вы действительно хотите, чтобы срок его действия истек, заключается в том, что если чей-то адрес электронной почты будет взломан через год (и они никогда не сбросят его), хакер получит право изменить пароль.
eduncan911 03
@ educationan911. Я знаю атаки методом грубой силы, но также, чтобы иметь доступ к хешированному ключу, человек с плохим намерением должен иметь доступ к электронной почте, и если у него есть доступ к нему, нет необходимости восстанавливать хешированный пароль. Кроме того, чтобы сделать это практически невозможным, вы можете хешировать хешированный пароль или, что еще лучше, хешировать пароль с чем-то еще. Я не возражаю с вами, я просто пытаюсь провести мозговой штурм по этому поводу
Дэниел
8

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

Предполагая, что у вас есть имя пользователя, пароль и рабочий адрес электронной почты, вам нужно добавить два поля в свою таблицу пользователей (если это таблица базы данных): дату с именем 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= < new_password_id >

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

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

Спасибо yada yada

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

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

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

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

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

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

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

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

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

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

Jmucchiello
источник
Разве потенциальный злоумышленник не сможет использовать MD5 текущей метки даты, чтобы войти?
Джордж Стокер
Я настоятельно не рекомендую отправлять пароль по электронной почте. Большинство пользователей оставляют эти электронные письма восстановленными, что является нарушением безопасности - некоторые из них захотят каждый раз просто копировать и вставлять его из своих «любимых» писем. Что делать, если срок действия сертификата почтового сервера компании пользователя истек и трафик прослушивается? Чтобы свести к минимуму это возможное нарушение, необходимо (1) установить короткий срок действия этого конкретного пароля - 1 час, и (2) заставить пользователя обновить его при следующем входе в систему.
Огнян Димитров
Огнян, пароль, отправленный по электронной почте, действует только один раз. Они должны изменить свой пароль после входа в систему, а электронное письмо не содержит имени пользователя для входа. Так что нет, они не могут просто копировать и вставлять его каждый раз. Не удаление электронной почты не является проблемой безопасности, поскольку это просто бессмысленная строка букв / цифр, которая НИЧЕГО не получит злоумышленнику после сброса пароля.
jmucchiello 05
3

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

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

Э.Дж. Бреннан
источник
2

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

Сергей Андреев
источник
Были бы полезны более подробные сведения.
Джордж Стокер,
2

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

Java Guy
источник
0

Я думаю, что руководство 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; }
}
Огглас
источник