Làm thế nào để cài đặt lại mật khẩu?


81

Tôi đang làm việc trên một ứng dụng trong ASP.NET và đang tự hỏi cụ thể làm cách nào tôi có thể triển khai một Password Resethàm nếu tôi muốn tự cuộn.

Cụ thể, tôi có những câu hỏi sau:

  • Cách tốt để tạo một ID duy nhất khó bẻ khóa là gì?
  • Nên có một bộ đếm thời gian gắn liền với nó? Nếu vậy thì phải là bao lâu?
  • Tôi có nên ghi lại địa chỉ IP không? Nó thậm chí không quan trọng?
  • Tôi nên yêu cầu thông tin gì trong màn hình "Đặt lại mật khẩu"? Chỉ địa chỉ Email? Hoặc có thể địa chỉ email cộng với một số thông tin mà họ 'biết'? (Đội yêu thích, tên chú cún, v.v.)

Có bất kỳ cân nhắc nào khác mà tôi cần biết không?

NB : Các câu hỏi khác đã được đề cập đến hoàn toàn về việc triển khai kỹ thuật. Thật vậy, câu trả lời được chấp nhận phủ bóng lên các chi tiết đẫm máu. Tôi hy vọng rằng câu hỏi này và các câu trả lời tiếp theo sẽ đi vào chi tiết đẫm máu, và tôi hy vọng bằng cách diễn đạt câu hỏi này hẹp hơn nhiều rằng các câu trả lời sẽ ít 'lông bông' và 'máu me hơn'.

Chỉnh sửa : Các câu trả lời cũng đi sâu vào cách một bảng như vậy sẽ được mô hình hóa và xử lý trong SQL Server hoặc bất kỳ liên kết ASP.NET MVC nào đến một câu trả lời sẽ được đánh giá cao.


ASP.NET MVC sử dụng nhà cung cấp xác thực ASP.NET mặc định, do đó, bất kỳ mẫu mã nào bạn tìm thấy xung quanh đó sẽ phải phù hợp với mục đích của bạn.
paulwhit

Câu trả lời:


66

Rất nhiều câu trả lời hay ở đây, tôi sẽ không bận tâm lặp lại tất cả ...

Ngoại trừ một vấn đề, được lặp lại bởi hầu hết các câu trả lời ở đây, mặc dù nó sai:

Guids là (thực tế) duy nhất và không thể đoán được về mặt thống kê.

Điều này không đúng, GUID là số nhận dạng rất yếu và KHÔNG được sử dụng để cho phép truy cập vào tài khoản của người dùng.
Nếu bạn kiểm tra cấu trúc, bạn nhận được tổng cộng tối đa là 128 bit ... ngày nay không được coi là nhiều.
Trong đó nửa đầu là bất biến điển hình (cho hệ thống tạo), và một nửa còn lại phụ thuộc vào thời gian (hoặc một cái gì đó tương tự).
Nói chung, nó là một cơ chế rất yếu và dễ bị cưỡng bức.

Vì vậy, đừng sử dụng cái đó!

Thay vào đó, chỉ cần sử dụng trình tạo số ngẫu nhiên mạnh về mặt mật mã ( System.Security.Cryptography.RNGCryptoServiceProvider) và nhận ít nhất 256 bit entropy thô.

Tất cả phần còn lại, như nhiều câu trả lời khác đã cung cấp.


6
Hoàn toàn đồng ý, theo như tôi biết, GUID không bao giờ được thiết kế để mật mã mạnh và không thể đoán được.
Jan Soltis

5
cũng nói, AFAIK MSDN tuyên bố rõ ràng rằng GUID không nên được sử dụng để bảo mật.
dr. ác

2
Các UUID phiên bản 4 đã được sử dụng trong Windows từ năm 2000: Các HƯỚNG DẪN .NET 4 được tạo ra như thế nào? - Tràn ngăn xếp . Chúng có 122 bit ngẫu nhiên trong đó, tôi nghĩ rằng phù hợp với các đề xuất của NIST. Có một lỗ hổng rất nặng đối với một cuộc tấn công cục bộ, theo CryptGenRandom - Wikipedia đã được sửa trong Vista và XP vào năm 2008. Vậy bạn thấy vấn đề với việc sử dụng GUID hiện tại ở đâu?
nealmcb

4
Blog "Old New Thing" đó đang mô tả các UUID phiên bản 1 không dùng nữa và trích dẫn một Bản nháp trên Internet (điều mà bạn không bao giờ nên làm) đã hết hạn vào năm 1998, 10 năm trước bài đăng trên blog. Tôi sẽ hoài nghi về chúng trong tương lai. Chúng tôi đã chiến đấu với những trận chiến đó từ lâu, và dường như đã thắng hầu hết chúng. Tôi vẫn đồng ý rằng việc sử dụng một cuộc gọi API sạch với một nguồn crypto-ngẫu nhiên là tốt hơn nhiều, nhưng không thể khá khó khăn như vậy trên GUID / UUIDs tại phiên bản 4.
nealmcb

1
Đối với những gì giá trị của nó, điều này không trả lời câu hỏi, "làm thế nào để đặt lại mật khẩu". Bạn vừa nôn ra những điểm tuyệt vời về GUIDs.
Rex Whitten vào

67

CHỈNH SỬA 2012/05/22: Theo dõi câu trả lời phổ biến này, tôi không còn tự sử dụng GUID trong quy trình này nữa. Giống như câu trả lời phổ biến khác, bây giờ tôi sử dụng thuật toán băm của riêng mình để tạo khóa để gửi URL. Điều này có lợi thế là ngắn hơn. Nhìn vào System.Security.Cryptography để tạo chúng, tôi cũng thường sử dụng SALT.

Đầu tiên, không đặt lại mật khẩu của người dùng ngay lập tức.

Đầu tiên, không đặt lại ngay mật khẩu của người dùng khi họ yêu cầu. Đây là một vi phạm bảo mật vì ai đó có thể đoán địa chỉ email (tức là địa chỉ email của bạn tại công ty) và đặt lại mật khẩu theo ý thích. Các phương pháp hay nhất hiện nay thường bao gồm một liên kết "xác nhận" được gửi đến địa chỉ email của người dùng, xác nhận rằng họ muốn đặt lại nó. Liên kết này là nơi bạn muốn gửi liên kết khóa duy nhất. Tôi gửi của tôi với một liên kết như:domain.com/User/PasswordReset/xjdk2ms92

Có, hãy đặt thời gian chờ trên liên kết và lưu trữ khóa và thời gian chờ trên chương trình phụ trợ của bạn (và muối nếu bạn đang sử dụng). Thời gian chờ 3 ngày là tiêu chuẩn và đảm bảo thông báo cho người dùng 3 ngày ở cấp web khi họ yêu cầu đặt lại.

Sử dụng một khóa băm duy nhất

Câu trả lời trước đây của tôi cho biết sử dụng GUID. Bây giờ tôi đang chỉnh sửa nó để khuyên mọi người sử dụng hàm băm được tạo ngẫu nhiên, ví dụ: sử dụng RNGCryptoServiceProvider. Và, hãy đảm bảo loại bỏ mọi "từ thực" khỏi hàm băm. Tôi nhớ lại một cuộc điện thoại đặc biệt lúc 6 giờ sáng, nơi một người phụ nữ nhận được một từ "c" nhất định trong khóa băm "giả sử là ngẫu nhiên" mà một nhà phát triển đã làm. Doh!

Toàn bộ thủ tục

  • Người dùng nhấp vào "đặt lại" mật khẩu.
  • Người dùng được yêu cầu cho một email.
  • Người dùng nhập email và bấm gửi. Không xác nhận hoặc từ chối email vì đây cũng là một hành vi xấu. Chỉ cần nói, "Chúng tôi đã gửi yêu cầu đặt lại mật khẩu nếu email được xác minh." hoặc một cái gì đó khó hiểu giống nhau.
  • Bạn tạo một băm từ RNGCryptoServiceProvider, lưu trữ nó dưới dạng một thực thể riêng biệt trong một ut_UserPasswordRequestsbảng và liên kết lại với người dùng. Vì vậy, điều này để bạn có thể theo dõi các yêu cầu cũ và thông báo cho người dùng rằng các liên kết cũ đã hết hạn.
  • Gửi liên kết đến email.

Người dùng nhận được liên kết, thích http://domain.com/User/PasswordReset/xjdk2ms92và nhấp vào liên kết đó.

Nếu liên kết được xác minh, bạn yêu cầu một mật khẩu mới. Đơn giản và người dùng có thể đặt mật khẩu của riêng họ. Hoặc, đặt mật khẩu khó hiểu của riêng bạn tại đây và thông báo cho họ về mật khẩu mới của họ tại đây (và gửi email cho họ).


1
Tôi đã tự hỏi, Nếu mật khẩu người dùng thực sự được băm, tại sao lại tạo một Khóa HASH mới? Sẽ không đúng nếu gửi email cho người dùng với liên kết đặt lại mật khẩu thông qua mật khẩu Hashed? Không thể hoàn nguyên mật khẩu đã băm, khi người dùng nhấp vào liên kết, máy chủ sẽ nhận được mật khẩu đã băm, so sánh với mật khẩu thực được lưu trữ và sau đó cho phép người dùng thay đổi mật khẩu.
Daniel

Và một điều thú vị khác về nó, là bạn không cần đặt Timeout, khi người dùng đã thay đổi mật khẩu, liên kết cũ sẽ tự động không còn hợp lệ nữa, vì mật khẩu băm được lưu trữ trong cơ sở dữ liệu đã bị thay đổi.
Daniel

@Daniel đó là một ý tưởng thực sự tồi. Tôi nghĩ rằng bạn cần Google thuật ngữ "các cuộc tấn công vũ phu". Ngoài ra, lý do bạn NÊN muốn nó hết hạn là trong trường hợp email của ai đó bị xâm phạm sau một năm (và họ không bao giờ đặt lại nó), hacker sẽ có quyền thay đổi mật khẩu.
eduncan911

@ educationan911. Tôi biết các cuộc tấn công brute force, ngoài ra, để có quyền truy cập vào khóa Hashed, Người có ý định xấu phải có quyền truy cập vào email và nếu anh ta có quyền truy cập vào đó, không cần phải hoàn nguyên mật khẩu đã băm. Ngoài ra, để làm cho nó gần như không thể, bạn có thể băm mật khẩu đã băm, hoặc thậm chí tốt hơn, băm mật khẩu bằng một thứ gì đó khác. Tôi không đồng ý với bạn, tôi chỉ đang cố gắng Động não về nó
Daniel

8

Trước tiên, chúng tôi cần biết những gì bạn đã biết về người dùng. Rõ ràng, bạn có tên người dùng và mật khẩu cũ. Bạn còn biết cái gì nữa? Bạn có địa chỉ email nào không? Bạn có dữ liệu về loài hoa yêu thích của người dùng không?

Giả sử bạn có tên người dùng, mật khẩu và địa chỉ email đang hoạt động, bạn cần thêm hai trường vào bảng người dùng của mình (giả sử đó là bảng cơ sở dữ liệu): một ngày gọi là new_passwd_expire và một chuỗi new_passwd_id.

Giả sử bạn có địa chỉ email của người dùng, khi ai đó yêu cầu đặt lại mật khẩu, bạn cập nhật bảng người dùng như sau:

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

Tiếp theo, bạn gửi email cho người dùng theo địa chỉ đó:

Gửi so-and-so

Ai đó đã yêu cầu mật khẩu mới cho tài khoản người dùng <tên người dùng> tại <tên trang web của bạn>. Nếu bạn đã yêu cầu đặt lại mật khẩu này, hãy nhấp vào liên kết sau:

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

Nếu liên kết đó không hoạt động, bạn có thể truy cập http://example.com/yourscript.lang và nhập thông tin sau vào biểu mẫu: <new_password_id>

Nếu bạn không yêu cầu đặt lại mật khẩu, bạn có thể bỏ qua email này.

Cảm ơn, yada yada

Bây giờ, mã hóa yourcript.lang: Tập lệnh này cần một biểu mẫu. Nếu bản cập nhật var được chuyển qua URL, biểu mẫu chỉ yêu cầu tên người dùng và địa chỉ email của người dùng. Nếu cập nhật không được thông qua, nó sẽ yêu cầu tên người dùng, địa chỉ email và mã id được gửi trong email. Bạn cũng yêu cầu một mật khẩu mới (tất nhiên là hai lần).

Để xác minh mật khẩu mới của người dùng, bạn xác minh tên người dùng, địa chỉ email và mã id đều khớp, yêu cầu chưa hết hạn và hai mật khẩu mới khớp nhau. Nếu thành công, bạn thay đổi mật khẩu của người dùng thành mật khẩu mới và xóa các trường đặt lại mật khẩu khỏi bảng người dùng. Ngoài ra, hãy đảm bảo đăng xuất người dùng / xóa mọi cookie liên quan đến đăng nhập và chuyển hướng người dùng đến trang đăng nhập.

Về cơ bản, trường new_passwd_id là mật khẩu chỉ hoạt động trên trang đặt lại mật khẩu.

Một cải tiến tiềm năng: bạn có thể xóa <tên người dùng> khỏi email. "Ai đó đã yêu cầu đặt lại mật khẩu cho tài khoản tại địa chỉ email này ...." Do đó, chỉ người dùng mới biết nếu email bị chặn. Tôi không bắt đầu theo cách đó vì nếu ai đó đang tấn công tài khoản, họ đã biết tên người dùng. Điều tối kỵ bổ sung này ngăn chặn các cuộc tấn công trung gian của cơ hội trong trường hợp ai đó có ý định xấu chặn email.

Đối với câu hỏi của bạn:

tạo chuỗi ngẫu nhiên: Nó không cần phải cực kỳ ngẫu nhiên. Bất kỳ trình tạo GUID nào hoặc thậm chí md5 (concat (salt, current_timestamp ())) là đủ, trong đó muối là thứ trên bản ghi người dùng như tài khoản dấu thời gian đã được tạo. Nó phải là thứ mà người dùng không thể nhìn thấy.

bộ đếm thời gian: Có, bạn cần cái này chỉ để giữ cho cơ sở dữ liệu của bạn khỏe mạnh. Không quá một tuần là thực sự cần thiết nhưng ít nhất là 2 ngày vì bạn không bao giờ biết thời gian trễ email có thể kéo dài bao lâu.

Địa chỉ IP: Vì email có thể bị trì hoãn hàng ngày, địa chỉ IP chỉ hữu ích cho việc ghi nhật ký chứ không phải để xác thực. Nếu bạn muốn đăng nhập nó, hãy làm như vậy, nếu không bạn không cần nó.

Đặt lại màn hình: Xem ở trên.

Hy vọng rằng bao gồm nó. Chúc may mắn.


Kẻ tấn công tiềm năng sẽ không thể sử dụng MD5 của dấu dữ liệu hiện tại để xâm nhập?
George Stocker

Tôi thực sự khuyên bạn không nên gửi mật khẩu qua email qua mạng. Hầu hết người dùng để lại những email này không bị xóa, đó là một vi phạm bảo mật - một số người trong số họ sẽ muốn sao chép và dán nó mỗi lần từ các email 'yêu thích' của họ. Điều gì sẽ xảy ra nếu chứng chỉ của máy chủ thư của công ty người dùng hết hạn và lưu lượng truy cập bị đánh hơi? Để giảm thiểu vi phạm có thể xảy ra này là (1) đặt thời gian hết hạn ngắn của mật khẩu cụ thể này - 1 giờ và (2) buộc người dùng cập nhật nó vào lần đăng nhập tiếp theo.
Ognyan Dimitrov

Ognyan, mật khẩu được gửi trong email chỉ hoạt động một lần. Họ phải thay đổi mật khẩu sau khi đăng nhập và email không có tên đăng nhập của người dùng. Vì vậy, không, họ không thể chỉ sao chép và dán nó mỗi lần. Không xóa email không phải là một vấn đề bảo mật vì nó chỉ là một chuỗi ký tự / số vô nghĩa sẽ khiến kẻ tấn công KHÔNG CÓ GÌ sau khi đặt lại mật khẩu.
jmucchiello

3

Một GUID được gửi đến địa chỉ email của hồ sơ có thể đủ cho hầu hết các ứng dụng chạy hàng loạt - với thời gian chờ thậm chí còn tốt hơn.

Rốt cuộc, nếu hộp thư điện tử của người dùng đã bị xâm nhập (tức là tin tặc có đăng nhập / mật khẩu cho địa chỉ email), bạn không thể làm gì nhiều về điều đó.


2

Bạn có thể gửi một email cho người dùng với một liên kết. Liên kết này sẽ chứa một số chuỗi khó đoán (như GUID). Ở phía máy chủ, bạn cũng sẽ lưu trữ cùng một chuỗi như bạn đã gửi cho người dùng. Bây giờ khi người dùng nhấn vào liên kết, bạn có thể tìm thấy trong mục nhập db của mình với cùng một chuỗi bí mật và đặt lại mật khẩu của nó.


Thêm chi tiết sẽ hữu ích.
George Stocker

2

1) Để tạo id duy nhất, bạn có thể sử dụng Thuật toán băm an toàn. 2) bộ đếm thời gian đính kèm? Ý của bạn là Hết hạn cho liên kết pwd đặt lại? Có, bạn có thể có bộ Ngày hết hạn 3) Bạn có thể yêu cầu thêm một số thông tin khác ngoài emailId để xác thực .. Như ngày sinh hoặc một số câu hỏi bảo mật 4) Bạn cũng có thể tạo các ký tự ngẫu nhiên và yêu cầu nhập ký tự đó cùng với yêu cầu .. để đảm bảo yêu cầu mật khẩu không bị tự động bởi một số phần mềm gián điệp hoặc những thứ tương tự ..


0

Tôi nghĩ rằng hướng dẫn của Microsoft cho ASP.NET Identity là một khởi đầu tốt.

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

Mã mà tôi sử dụng cho ASP.NET Identity:

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; }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.