Як здійснити скидання пароля?


81

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

У мене є такі запитання:

  • Який хороший спосіб створення унікального ідентифікатора, який важко зламати?
  • Чи повинен бути прикріплений до нього таймер? Якщо так, то як довго це має бути?
  • Чи слід записувати IP-адресу? Це взагалі має значення?
  • Яку інформацію слід запитати на екрані "Скидання пароля"? Просто адреса електронної пошти? А може, електронна адреса та якась інформація, яку вони "знають"? (Улюблена команда, ім’я цуценя тощо)

Чи є якісь інші міркування, про які мені слід знати?

NB : Інші питання були замовчується технічної реалізації повністю. Дійсно, прийнята відповідь охоплює криваві деталі. Я сподіваюся, що це питання та наступні відповіді будуть входити в криваві подробиці, і я сподіваюся, формулюючи це питання набагато вужче, що відповіді будуть менш „пухнастими” та більше „кровними”.

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


ASP.NET MVC використовує провайдера автентифікації ASP.NET за замовчуванням, тому будь-які зразки коду, які ви знайдете навколо цього, повинні бути актуальними для ваших цілей.
paulwhit

Відповіді:


66

Тут багато хороших відповідей, я не буду турбуватися, все це повторюючи ...

За винятком одного питання, яке повторюється майже кожною відповіддю тут, навіть якщо воно неправильне:

Посібники є (реально) унікальними і статистично неможливо вгадати.

Це неправда, GUID - це дуже слабкі ідентифікатори, і їх НЕ слід використовувати для надання доступу до облікового запису користувача.
Якщо ви вивчите структуру, ви отримаєте в загальній складності не більше 128 бітів ... що сьогодні не враховується багато.
З них перша половина є типовим інваріантом (для генеруючої системи), а половина того, що залишилось, залежить від часу (або щось інше подібне).
Загалом, це дуже слабкий і легко підданий жорстокому механізму механізм.

Тож не використовуйте цього!

Натомість просто використовуйте криптографічно сильний генератор випадкових чисел ( System.Security.Cryptography.RNGCryptoServiceProvider) і отримайте принаймні 256 біт необробленої ентропії.

Усі інші, як і численні інші відповіді.


6
Абсолютно погоджуюсь, наскільки мені відомо, GUID ніколи не були розроблені таким чином, щоб бути криптографічно міцними і неможливо вгадати.
Ян Солтіс,

5
Добре сказано, AFAIK MSDN чітко заявляє, що GUID не слід використовувати для безпеки.
доктор злий

2
UUID версії 4 використовуються в Windows з 2000 року: Як генеруються GUID .NET 4? - Переповнення стека . У них 122 випадкові біти, що, на мою думку, відповідає рекомендаціям NIST. Існувала дуже погана вразливість до локальної атаки, яка, згідно з CryptGenRandom - Вікіпедія була виправлена ​​у Vista та XP до 2008 року. Тож де ви бачите проблеми із поточним використанням GUID?
nealmcb

4
Цей блог "Стара нова річ" описує застарілі UUID версії 1 та посилається на проект Інтернету (те, що ви ніколи не повинні робити), термін дії якого закінчився у 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, і натискає його.

Якщо посилання перевірено, ви запитуєте новий пароль. Просто, і користувач може встановити власний пароль. Або встановіть тут свій власний загадковий пароль і повідомте їм про новий пароль тут (і надішліть його електронною поштою).


1
Я дивувався, якщо фактичний пароль користувача хешований, навіщо створювати новий ключ HASH? Неправильно відправляти електронне повідомлення користувачеві із посиланням для скидання пароля, передаючи хешований пароль? Хешований пароль не можна повернути назад, коли користувач натискає на посилання, сервер отримає хешований пароль, порівняє з фактично збереженим, а потім дозволить користувачеві змінити пароль.
Даніель

І ще одна, напевно, хороша річ у тому, що вам не потрібно встановлювати тайм-аут, як тільки користувач змінив пароль, старе посилання автоматично більше не буде дійсним, оскільки змінений хешований пароль, що зберігається в базі даних.
Даніель

@Daniel, це дійсно погана ідея. Я думаю, вам потрібно погуглити термін "атаки грубої сили". Крім того, причиною того, що ви дійсно хочете, щоб термін його дії закінчився, є те, що якщо хтось електронну пошту буде скомпрометовано через рік (і вони ніколи її не скинуть), хакер отримує права змінювати пароль.
eduncan911

@ educan911. Я знаю атаки грубої сили, але також, щоб мати доступ до хешованого ключа, Погано навмисна людина, повинен мати доступ до електронної пошти, і якщо він має доступ до цього, немає необхідності повертати хешований пароль. Крім того, щоб зробити це майже неможливим, ви можете хешувати хешований пароль, а ще краще - хешувати пароль чимось більше. Я не погоджуюся з вами, я просто намагаюся зробити про це "Мозковий штурм"
Даніель

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>

Якщо ви не вимагали скидання пароля, ви можете проігнорувати цей електронний лист.

Дякую, яда яда

Тепер кодування yourcript.lang: Цей сценарій потребує форми. Якщо оновлення var передано за URL-адресою, форма просто запитує ім’я користувача та електронну адресу користувача. Якщо оновлення не передано, він запитує ім’я користувача, електронну адресу та ідентифікаційний код, надіслані в електронному листі. Ви також запитуєте новий пароль (звичайно, двічі).

Щоб перевірити новий пароль користувача, ви підтверджуєте, що ім’я користувача, електронна адреса та ідентифікаційний код збігаються, що запит не минув та що два нових паролі збігаються. У разі успіху ви зміните пароль користувача на новий і очистите поля скидання пароля з таблиці користувачів. Також не забудьте вийти з користувача / очистити всі файли cookie, пов’язані з входом, і перенаправити користувача на сторінку входу.

По суті, поле new_passwd_id - це пароль, який працює лише на сторінці скидання пароля.

Одне з можливих покращень: ви можете видалити <ім'я користувача> з електронного листа. "Хтось вимагає скидання пароля для облікового запису за цією електронною адресою ...." Таким чином, роблячи ім'я користувача тим, що лише користувач знає, чи електронний лист перехоплюється. Я не починав з цього шляху, тому що якщо хтось атакує рахунок, він вже знає ім’я користувача. Ця додаткова неясність зупиняє атаки можливостей "посередині", якщо хтось зловмисний перехопить електронну пошту.

Щодо ваших питань:

генерація випадкового рядка: він не повинен бути надзвичайно випадковим. Достатньо будь-якого генератора GUID або навіть md5 (concat (salt, current_timestamp ())), де сіль є чимось на записі користувача, наприклад, був створений рахунок часу. Це має бути те, що користувач не бачить.

таймер: Так, це потрібно для того, щоб зберегти свою базу даних у здоровому стані. Насправді потрібно не більше тижня, але принаймні 2 дні, оскільки ви ніколи не знаєте, скільки може тривати затримка електронної пошти.

IP-адреса. Оскільки електронна адреса може затримуватися на дні, IP-адреса корисна лише для реєстрації, а не для перевірки. Якщо ви хочете зареєструвати його, зробіть це, інакше це вам не потрібно.

Екран скидання: Див. Вище.

Сподіваюся, що це покриває. Удачі.


Чи не зможе потенційний зловмисник використати MD5 поточної позначки дати, щоб потрапити?
Джордж Стокер, 02

Я настійно не рекомендую надсилати пароль електронною поштою по дроту. Більшість користувачів залишають ці електронні листи не видаленими, що є порушенням безпеки - деякі з них хотіли б просто скопіювати та вставити його щоразу зі своїх "улюблених" електронних листів. Що робити, якщо термін дії сертифіката поштового сервера компанії користувачів закінчився і трафік нюхається? Щоб мінімізувати це можливе порушення - це (1) встановити короткий час дії цього конкретного пароля - 1 година, і (2) змусити користувача оновити його під час наступного входу.
Огнян Димитров

Огнян, пароль, надісланий електронною поштою, працює лише один раз. Вони повинні змінити свій пароль після входу, і електронна пошта не містить імені користувача для входу. Отже, ні, вони не можуть просто копіювати-вставляти його кожен раз. Не видалення електронної пошти не є проблемою безпеки, оскільки це лише безглуздий рядок букв / цифр, який нічого НЕ отримає зловмиснику після скидання пароля.
jmucchiello

3

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

Зрештою, якщо поштова скринька користувачів була скомпрометована (тобто хакер має вхід / пароль для електронної адреси), з цим мало що можна зробити.


2

Ви можете надіслати електронне повідомлення користувачеві із посиланням. Це посилання міститиме важкий для вгадування рядок (наприклад GUID). На стороні сервера ви також зберігаєте той самий рядок, що і надіслали користувачеві. Тепер, коли користувач натискає на посилання, ви можете знайти у своєму записі db той самий секретний рядок і скинути його пароль.


Більше деталей було б корисно.
Джордж Стокер

2

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


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; }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.