web-dev-qa-db-ja.com

パスワードリセットを実装する方法

私はASP.NETのアプリケーションに取り組んでいますが、Password Reset関数を自分でロールしたい場合。

具体的には、次の質問があります。

  • クラックしにくい一意のIDを生成する良い方法は何ですか?
  • タイマーを接続する必要がありますか?もしそうなら、それはどのくらいの長さですか?
  • IPアドレスを記録する必要がありますか?それも重要ですか?
  • [パスワードのリセット]画面でどのような情報を要求する必要がありますか?メールアドレスだけ?または、メールアドレスに加えて、彼らが「知っている」情報の一部でしょうか? (お気に入りのチーム、子犬の名前など)

知っておく必要がある他の考慮事項はありますか?

[〜#〜] nb [〜#〜]その他の質問 have glossed over technical完全に実装。確かに、受け入れられた答えは、つまらない詳細をglossめています。この質問とその後の回答が詳細な内容になることを望み、この質問をより狭義に表現することで、答えが「ふわふわ」ではなく「マチ」になることを願っています。

Edit:SQL Serverまたは回答へのASP.NET MVCリンクでこのようなテーブルがどのようにモデル化され処理されるかについての回答も歓迎します。

81
George Stocker

ここにはたくさんの良い答えがあります。

1つの問題を除いて、それはここでほとんどすべての答えによって繰り返されます、それが間違っていても:

ガイドは(現実的に)一意であり、統計的に推測することは不可能です。

これは真実ではありません。GUIDは非常に弱い識別子であり、ユーザーアカウントへのアクセスを許可するために[〜#〜] not [〜#〜]を使用する必要があります。
構造を調べると、合計で最大で128ビットが得られます。これは最近ではあまり考慮されていません。
前半は典型的な不変量(生成システムの場合)で、残りの半分は時間依存(または他の類似のもの)です。
全体として、非常に弱く、簡単にブルートフォースされるメカニズムです。

それを使用しないでください!

代わりに、暗号的に強力な乱数ジェネレーター(System.Security.Cryptography.RNGCryptoServiceProvider)、少なくとも256ビットの生エントロピーを取得します。

他の多くの答えが提供したように、残りすべて。

66
AviD

編集2012/05/22:この人気のある回答のフォローアップとして、この手順ではGUIDを使用しなくなりました。他の一般的な回答と同様に、私は独自のハッシュアルゴリズムを使用して、URLで送信するキーを生成します。これには、短いという利点もあります。 System.Security.Cryptographyを調べて生成します。通常はSALTも使用します。

まず、ユーザーのパスワードをすぐにリセットしないでください。

まず、ユーザーが要求したときにユーザーのパスワードをすぐにリセットしないでください。誰かが電子メールアドレス(つまり、会社の電子メールアドレス)を推測し、気まぐれにパスワードをリセットできるため、これはセキュリティ違反です。最近のベストプラクティスには、通常、ユーザーの電子メールアドレスに送信される「確認」リンクが含まれ、ユーザーはそれをリセットすることを確認します。このリンクは、一意のキーリンクを送信する場所です。私は次のようなリンクで私のものを送ります:domain.com/User/PasswordReset/xjdk2ms92

はい、リンクにタイムアウトを設定し、バックエンドにキーとタイムアウトを保存します(使用している場合はソルト)。 3日間のタイムアウトが標準であり、ユーザーがリセットを要求するときは、Webレベルで3日間通知するようにしてください。

一意のハッシュキーを使用する

以前の回答では、GUIDを使用するように言っていました。私は今これを編集して、ランダムに生成されたハッシュを使用するようみんなに勧めています。 RNGCryptoServiceProviderを使用します。また、ハッシュから「実際の単語」をすべて削除してください。私は、女性が開発者が行った「ランダムであると思われる」ハッシュキーで特定の「c」ワードを受け取ったという午前6時の特別な電話を思い出します。ドッ!

手順全体

  • ユーザーが「リセット」パスワードをクリックします。
  • ユーザーはメールを求められます。
  • ユーザーがメールを入力し、送信をクリックします。これも悪い習慣であるため、メールを確認または拒否しないでください。 「メールが確認されたら、パスワードリセットリクエストを送信しました」と言うだけです。または不可解な何か。
  • RNGCryptoServiceProviderからハッシュを作成し、別のエンティティとしてut_UserPasswordRequestsテーブルに保存して、ユーザーにリンクします。これにより、古いリクエストを追跡し、古いリンクが期限切れになったことをユーザーに通知できます。
  • メールへのリンクを送信します。

ユーザーはhttp://domain.com/User/PasswordReset/xjdk2ms92などのリンクを取得してクリックします。

リンクが検証されたら、新しいパスワードを要求します。シンプルで、ユーザーは自分のパスワードを設定できます。または、ここで独自の暗号化パスワードを設定し、新しいパスワードをここで通知します(そしてメールで送信します)。

67
eduncan911

最初に、ユーザーについて既に知っていることを知る必要があります。明らかに、ユーザー名と古いパスワードがあります。他に何を知っていますか?メールアドレスはありますか?ユーザーのお気に入りの花に関するデータはありますか?

ユーザー名、パスワード、および有効な電子メールアドレスを持っていると仮定すると、ユーザーテーブルに2つのフィールドを追加する必要があります(データベーステーブルであると仮定):new_passwd_expireと呼ばれる日付と文字列new_passwd_id。

ユーザーの電子メールアドレスがわかっている場合、誰かがパスワードのリセットを要求すると、次のようにユーザーテーブルを更新します。

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

次に、そのアドレスのユーザーにメールを送信します。

親愛なるまあまあ

誰かが<your website name>のユーザーアカウント<username>の新しいパスワードを要求しました。このパスワードのリセットをリクエストした場合は、次のリンクに従ってください。

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

そのリンクが機能しない場合は、 http://example.com/yourscript.lang に移動し、フォームに次のように入力します:<new_password_id>

パスワードのリセットをリクエストしなかった場合、このメールは無視してください。

ありがとう、やだやだ

さて、yourscript.langのコーディング:このスクリプトにはフォームが必要です。 varの更新がURLで渡された場合、フォームはユーザーのユーザー名と電子メールアドレスを要求するだけです。更新が渡されない場合、ユーザー名、電子メールアドレス、および電子メールで送信されたIDコードを要求します。また、新しいパスワードを要求します(もちろん2回)。

ユーザーの新しいパスワードを確認するには、ユーザー名、メールアドレス、IDコードがすべて一致していること、リクエストの有効期限が切れていないこと、2つの新しいパスワードが一致していることを確認します。成功した場合、ユーザーのパスワードを新しいパスワードに変更し、ユーザーテーブルからパスワードリセットフィールドをクリアします。また、必ずユーザーをログアウト/ログイン関連のCookieをクリアして、ユーザーをログインページにリダイレクトしてください。

基本的に、new_passwd_idフィールドはパスワードリセットページでのみ機能するパスワードです。

1つの潜在的な改善:<username>を電子メールから削除できます。 「誰かがこの電子メールアドレスのアカウントのパスワードリセットを要求しました。..」したがって、電子メールが傍受された場合、ユーザーのみが知っているユーザー名になります。誰かがアカウントを攻撃している場合、そのユーザーは既にユーザー名を知っているので、私はその方法から始めませんでした。この追加されたあいまいさにより、悪意のある人がメールを傍受した場合に備えて、中間者による機会攻撃が阻止されます。

ご質問について:

ランダムな文字列の生成:極端にランダムである必要はありません。任意のGUID generatorまたはmd5(concat(salt、current_timestamp()))で十分です。ここで、saltはタイムスタンプアカウントが作成されたようなユーザーレコード上のものです。ユーザーができるものでなければなりませんわからない。

タイマー:はい、データベースを正常に保つためだけにこれが必要です。本当に必要なのは1週間以内ですが、メールの遅延がどれくらい続くかわからないため、少なくとも2日間は必要です。

IPアドレス:電子メールは数日遅れる可能性があるため、IPアドレスはログ記録にのみ役立ち、検証には役立ちません。ログに記録したい場合は記録してください。そうでない場合は必要ありません。

リセット画面:上記を参照してください。

それをカバーすることを願っています。幸運を。

8
jmucchiello

A GUIDレコードの電子メールアドレスに送信すれば、ほとんどの従来のアプリケーションに十分な可能性があります-タイムアウトはさらに改善されます。

結局のところ、ユーザーのメールボックスが侵害された場合(つまり、ハッカーがメールアドレスのログオン/パスワードを持っている場合)、それについてできることはあまりありません。

3
E.J. Brennan

リンクを記載したメールをユーザーに送信できます。このリンクには、推測しにくい文字列(GUIDなど)が含まれます。サーバー側では、ユーザーに送信したものと同じ文字列も保存します。ユーザーがリンクを押すと、同じシークレット文字列を使用してdbエントリを検索し、パスワードをリセットできます。

2
Sergej Andrejev

1)一意のIDを生成するには、Secure Hash Algorithmを使用できます。 2)タイマーが取り付けられていますか?リセットpwdリンクの有効期限を意味しましたか?はい。有効期限を設定できます。3)検証するためにemailId以外の情報を要求できます。生年月日やセキュリティの質問のように4)ランダムな文字を生成して、要求とともに入力することもできます。 ..パスワード要求がスパイウェアなどによって自動化されないようにするため..

2
Java Guy

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