비밀번호 재설정을 구현하는 방법?


81

ASP.NET에서 응용 프로그램을 작업 중이며 Password Reset내 자신을 롤링하려는 경우 함수를 구현할 수있는 방법이 구체적으로 궁금합니다 .

특히 다음과 같은 질문이 있습니다.

  • 해독하기 어려운 고유 ID를 생성하는 좋은 방법은 무엇입니까?
  • 타이머가 부착되어 있어야합니까? 그렇다면 얼마나 걸리나요?
  • IP 주소를 기록해야합니까? 그것이 중요합니까?
  • "비밀번호 재설정"화면에서 어떤 정보를 요청해야합니까? 이메일 주소 만? 아니면 이메일 주소와 그들이 '알고있는'정보의 일부일까요? (좋아하는 팀, 강아지 이름 등)

알아야 할 다른 고려 사항이 있습니까?

NB : 기술적 구현에 대한 다른 질문 은 완전히 사라졌습니다 . 실제로 받아 들여지는 대답은 피투성이의 세부 사항에 대해 광택이 있습니다. 이 질문과 후속 답변이 피투성이의 세부 사항으로 들어가기를 바랍니다.이 질문을 훨씬 더 좁게 표현하여 답변이 '보풀'이 적고 '고어'가 되길 바랍니다.

편집 : SQL Server 또는 답변에 대한 ASP.NET MVC 링크에서 그러한 테이블을 모델링하고 처리하는 방법에 대한 답변도 감사하겠습니다.


ASP.NET MVC는 기본 ASP.NET 인증 공급자를 사용하므로 주변에서 찾은 모든 코드 샘플은 사용자의 목적과 관련이 있어야합니다.
paulwhit 2009

답변:


66

여기에 좋은 답변이 많이 있습니다. 모든 것을 반복하지 않을 것입니다.

한 가지 문제를 제외하고는 여기에 거의 모든 답변이 반복됩니다.

가이드는 (현실적으로) 독특하고 통계적으로 추측하기 불가능합니다.

이 GUID가 매우 약한 식별자, 사실이 아니다, 그리고해야 하지 사용자 계정에 대한 액세스를 허용 할 수.
구조를 살펴보면 기껏해야 총 128 비트를 얻을 수 있습니다. 요즘에는 많이 고려되지 않습니다.
그중 전반부는 전형적인 불변이며 (생성 시스템의 경우) 나머지 절반은 시간 의존적입니다 (또는 다른 유사한 것).
대체로 매우 약하고 쉽게 무차별 대입되는 메커니즘입니다.

그러니 사용하지 마세요!

대신 암호 학적으로 강력한 난수 생성기 ( System.Security.Cryptography.RNGCryptoServiceProvider)를 사용하고 최소 256 비트의 원시 엔트로피를 얻으십시오.

나머지는 다른 수많은 답변이 제공했습니다.


6
내가 아는 한 GUID는 암호 학적으로 강력하고 추측하기 불가능하도록 설계되지 않았습니다.
Jan Soltis

5
잘 말하면 AFAIK MSDN은 GUID를 보안에 사용해서는 안된다고 분명히 명시합니다.
박사. 악

2
버전 4 UUID는 2000 년부터 Windows에서 사용되었습니다. .NET 4 GUID는 어떻게 생성됩니까? -스택 오버플로 . 그들은 122 개의 임의의 비트를 가지고 있으며, NIST 권장 사항에 부합한다고 생각합니다. CryptGenRandom-Wikipedia 에 따르면 2008 년까지 Vista 및 XP에서 수정 된 로컬 공격에 대한 매우 나쁜 취약성이있었습니다 . 현재 GUID 사용에 문제가있는 곳은 어디입니까?
nealmcb 2011 년

4
이 "Old New Thing"블로그는 더 이상 사용되지 않는 버전 1 UUID를 설명하고 있으며 블로그 게시물 10 년 전인 1998 년에 만료 된 Internet Draft (당신이 절대로하지 말아야 할 일)를 인용합니다. 나는 미래에 그들에 대해 회의적 일 것입니다. 우리는 오래 전에 그 전투에서 싸웠으며 대부분의 전투에서 승리 한 것 같습니다. 난 여전히 암호 랜덤 소스에 깨끗한 API 호출을 사용하는 것이 훨씬 낫다는 것을 동의하지만, 아주 열심히 GUID에하지 말라 / 버전 4에서 UUID를
nealmcb

1
그 가치가 무엇인지, 이것은 "비밀번호를 재설정하는 방법"이라는 질문에 답하지 않습니다. GUID에 대한 좋은 점을 토로했습니다.
Rex Whitten 2014

67

2012/05/22 편집 :이 인기있는 답변에 대한 후속 조치로이 절차에서 더 이상 GUID를 직접 사용하지 않습니다. 다른 인기있는 답변과 마찬가지로 이제는 자체 해싱 알고리즘을 사용하여 URL로 보낼 키를 생성합니다. 이것은 또한 더 짧다는 장점이 있습니다. System.Security.Cryptography를 살펴보고 생성하는 데 일반적으로 SALT도 사용합니다.

첫째, 사용자의 비밀번호를 즉시 재설정하지 마십시오.

첫째, 사용자가 요청할 때 즉시 암호를 재설정하지 마십시오. 누군가가 이메일 주소 (예 : 회사의 이메일 주소)를 추측하고 비밀번호를 재설정 할 수 있으므로 이는 보안 위반입니다. 요즘의 모범 사례에는 일반적으로 사용자의 이메일 주소로 전송되는 "확인"링크가 포함되어 재설정 의사를 확인합니다. 이 링크는 고유 키 링크를 보낼 곳입니다. 나는 다음과 같은 링크로 내 보낸다.domain.com/User/PasswordReset/xjdk2ms92

예, 링크에 시간 제한을 설정하고 키와 시간 제한을 백엔드에 저장하십시오 (사용중인 경우 솔트도 포함). 3 일의 제한 시간이 일반적이며 사용자가 재설정을 요청할 때 웹 수준에서 3 일을 알려야합니다.

고유 한 해시 키 사용

내 이전 답변은 GUID를 사용한다고 말했습니다. 나는 모든 사람들에게 무작위로 생성 된 해시를 사용하도록 조언하기 위해 이것을 편집하고 RNGCryptoServiceProvider있습니다. 그리고 해시에서 "실제 단어"를 제거해야합니다. 나는 여성이 개발자가 한 "무작위로 가정"해시 키에서 특정 "c"단어를받은 특별한 오전 6시 전화를 기억합니다. 도!

전체 절차

  • 사용자가 "재설정"암호를 클릭합니다.
  • 사용자에게 이메일을 요청합니다.
  • 사용자가 이메일을 입력하고 보내기를 클릭합니다. 이것은 나쁜 습관이므로 이메일을 확인하거나 거부하지 마십시오. "이메일이 확인되면 비밀번호 재설정 요청을 보냈습니다."라고 말하면됩니다. 또는 비슷하게 비밀스러운 것.
  • 에서 해시를 만들고 RNGCryptoServiceProvider이를 ut_UserPasswordRequests테이블에 별도의 엔터티로 저장 하고 사용자에게 다시 연결합니다. 따라서 이전 요청을 추적하고 사용자에게 이전 링크가 만료되었음을 알릴 수 있습니다.
  • 링크를 이메일로 보냅니다.

사용자가와 같은 링크를 http://domain.com/User/PasswordReset/xjdk2ms92가져와 클릭합니다.

링크가 확인되면 새 비밀번호를 요청합니다. 간단하고 사용자는 자신의 암호를 설정할 수 있습니다. 또는 여기에서 자신의 비밀 암호를 설정하고 여기에서 새 암호를 알리고 이메일을 보내십시오.


1
궁금한 점이 있습니다. 실제 사용자 암호가 해시 된 경우 새 해시 키를 생성하는 이유는 무엇입니까? 해시 된 비밀번호를 전달하는 비밀번호 재설정 링크가있는 이메일을 사용자에게 보내는 것이 올바르지 않습니까? 해시 된 비밀번호는 되돌릴 수 없으며 사용자가 링크를 클릭하면 서버가 해시 된 비밀번호를 수신하고 실제 저장된 비밀번호와 비교 한 다음 사용자가 비밀번호를 변경할 수 있도록 허용합니다.
Daniel

또 다른 좋은 점은 Timeout을 설정할 필요가 없다는 것입니다. 일단 사용자가 암호를 변경하면 데이터베이스에 저장된 해시 된 암호가 변경 되었기 때문에 이전 링크가 자동으로 더 이상 유효하지 않게됩니다.
Daniel

@Daniel 정말 나쁜 생각입니다. 구글에서 "무력 공격"이라는 용어가 필요하다고 생각합니다. 또한 만료되기를 바라는 이유는 누군가의 이메일이 1 년 후에 손상 될 경우 (그리고 절대 재설정하지 않는 경우) 해커가 암호를 변경할 권한을 얻습니다.
eduncan911 2015 년

@ educan911. 나는 무차별 대입 공격을 알고 있지만 Hashed 키에 액세스하려면 Bad Intented 사람이 이메일에 액세스해야하며, 이메일에 액세스 할 수있는 경우 해시 된 암호를 되돌릴 필요가 없습니다. 또한 거의 불가능하게하려면 해시 된 암호를 해시하거나 더 나은 방법으로 암호를 더 많은 것으로 해시 할 수 있습니다. 나는 당신의 의견에 동의하지 않습니다. 단지 그것에 대해 브레인 스토밍을하려고합니다
Daniel

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>

비밀번호 재설정을 요청하지 않으 셨다면이 이메일을 무시 하셔도됩니다.

고마워 야다 야다

이제 yourscript.lang 코딩 :이 스크립트에는 양식이 필요합니다. var 업데이트가 URL에 전달 된 경우 양식은 사용자의 사용자 이름과 이메일 주소 만 묻습니다. 업데이트가 통과되지 않으면 사용자 이름, 이메일 주소 및 이메일로 전송 된 ID 코드를 요청합니다. 또한 새 비밀번호를 요청합니다 (물론 두 번).

사용자의 새 비밀번호를 확인하려면 사용자 이름, 이메일 주소 및 ID 코드가 모두 일치하는지, 요청이 만료되지 않았는지, 두 개의 새 비밀번호가 일치하는지 확인합니다. 성공하면 사용자의 암호를 새 암호로 변경하고 사용자 테이블에서 암호 재설정 필드를 지 웁니다. 또한 사용자를 로그 아웃하거나 로그인 관련 쿠키를 지우고 사용자를 로그인 페이지로 리디렉션하십시오.

기본적으로 new_passwd_id 필드는 비밀번호 재설정 페이지에서만 작동하는 비밀번호입니다.

한 가지 잠재적 개선 사항 : 이메일에서 <username>을 제거 할 수 있습니다. "누군가가이 이메일 주소로 계정에 대한 비밀번호 재설정을 요청했습니다 ..."따라서 이메일이 도청 된 경우 사용자 만 아는 사용자 이름이됩니다. 누군가가 계정을 공격하는 경우 이미 사용자 이름을 알고 있기 때문에 그렇게 시작하지 않았습니다. 이렇게 추가 된 모호성은 악의적 인 누군가가 이메일을 가로채는 경우 중간자 (man-in-the-middle) 기회 공격을 차단합니다.

귀하의 질문 :

무작위 문자열 생성 : 매우 무작위 일 필요는 없습니다. 모든 GUID 생성기 또는 md5 (concat (salt, current_timestamp ()))이면 충분합니다. 여기서 salt는 타임 스탬프 계정이 생성 된 것과 같은 사용자 레코드에있는 것입니다. 사용자가 볼 수없는 것이어야합니다.

타이머 : 예, 데이터베이스를 정상 상태로 유지하려면이 기능이 필요합니다. 일주일 이상은 필요하지 않지만 이메일 지연이 얼마나 오래 지속 될지 모르기 때문에 최소 2 일은 필요합니다.

IP 주소 : 이메일이 며칠 지연 될 수 있으므로 IP 주소는 유효성 검사가 아닌 로깅에만 유용합니다. 기록하고 싶다면 그렇게하세요. 그렇지 않으면 필요하지 않습니다.

화면 재설정 : 위를 참조하십시오.

그것이 그것을 덮기를 바랍니다. 행운을 빕니다.


잠재적 인 공격자가 현재 날짜 기록의 MD5를 사용하여 침입 할 수 없습니까?
George Stocker

유선으로 이메일로 비밀번호를 보내지 않는 것이 좋습니다. 대부분의 사용자는 이러한 이메일을 삭제하지 않은 채로 두어 보안 위반입니다. 일부 사용자는 '좋아하는'이메일에서 매번 복사하여 붙여 넣기를 원할 것입니다. 사용자 회사 메일 서버의 인증서가 만료되고 트래픽이 스니핑되면 어떻게됩니까? 이러한 위반 가능성을 최소화하려면 (1)이 특정 암호의 짧은 만료 시간 (1 시간)을 설정하고 (2) 사용자가 다음에 로그온 할 때 업데이트하도록 강제하는 것입니다.
Ognyan Dimitrov 2014

Ognyan, 이메일로 전송 된 비밀번호는 한 번만 작동합니다. 로그인 후 비밀번호를 변경해야하며 이메일에는 사용자 로그인 이름이 포함되어 있지 않습니다. 따라서 매번 복사하여 붙여 넣을 수는 없습니다. 이메일을 삭제하지 않는 것은 암호가 재설정 된 후에도 공격자가 얻을 수없는 의미없는 문자 / 숫자 문자열이기 때문에 보안 문제가 아닙니다.
jmucchiello 2014

3

기록의 이메일 주소로 전송 된 GUID는 대부분의 평범한 응용 프로그램에 충분할 가능성이 높으며 제한 시간이 훨씬 더 좋습니다.

결국, 사용자의 이메일 박스가 훼손된 경우 (즉, 해커가 이메일 주소에 대한 로그온 / 암호를 가지고 있음) 이에 대해 할 수있는 일이별로 없습니다.


2

링크가있는 사용자에게 이메일을 보낼 수 있습니다. 이 링크에는 추측하기 어려운 문자열 (예 : GUID)이 포함됩니다. 서버 측에서도 사용자에게 보낸 것과 동일한 문자열을 저장합니다. 이제 사용자가 링크를 누르면 동일한 비밀 문자열이있는 db 항목을 찾아 비밀번호를 재설정 할 수 있습니다.


자세한 내용은 도움이 될 것입니다.
George Stocker

2

1) 고유 ID를 생성하려면 Secure Hash Algorithm을 사용할 수 있습니다. 2) 타이머 부착? 재설정 암호 링크에 대한 만료를 의미 했습니까? 예, 만료 설정을 할 수 있습니다. 3) 이메일 ID 이외의 다른 정보를 확인하여 요청할 수 있습니다. 생년월일 또는 보안 질문과 같이 4) 임의의 문자를 생성하고 요청과 함께 입력하도록 요청할 수도 있습니다. .. 암호 요청이 일부 스파이웨어 또는 이와 유사한 것에 의해 자동화되지 않았는지 확인합니다.


0

ASP.NET Identity에 대한 Microsoft 가이드가 좋은 시작이라고 생각합니다.

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

ASP.NET ID에 사용하는 코드 :

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; }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.