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


81

私はASP.NETでアプリケーションに取り組んでおり、自分でPassword Reset関数を作成したい場合、どのように関数を実装できるのか具体的に考えていました。

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

  • 解読しにくい一意のIDを生成する良い方法は何ですか?
  • タイマーを付ける必要がありますか?もしそうなら、それはどのくらいの期間である必要がありますか?
  • IPアドレスを記録する必要がありますか?それも重要ですか?
  • 「パスワードのリセット」画面でどのような情報を要求すればよいですか?メールアドレスだけ?それとも、メールアドレスと彼らが「知っている」情報の一部ですか?(好きなチーム、子犬の名前など)

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

注意他の質問技術的な実装を完全に覆い隠しています。確かに、受け入れられた答えは、残酷な詳細を覆い隠します。この質問とその後の回答が厄介な詳細に入るといいのですが、この質問をもっと狭く表現することで、回答が「ふわふわ」ではなく「マチ」になることを願っています。

編集:SQLServerまたは回答へのASP.NETMVCリンクでそのようなテーブルがどのようにモデル化および処理されるかについても説明する回答をいただければ幸いです。


ASP.NET MVCはデフォルトのASP.NET認証プロバイダーを使用するため、その周辺で見つかったコードサンプルは、目的に関連するまで必要です。
paulwhit 2009

回答:


66

ここにはたくさんの良い答えがあります、私はそれをすべて繰り返すことを気にしません...

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

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

これは正しくありません。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によると、ローカル攻撃には非常に悪い脆弱性がありました。ウィキペディアは2008年までにVistaとXPで修正されました。では、現在のGUIDの使用に関する問題はどこにありますか。
nealmcb 2011年

4
その「OldNewThing」ブログは、非推奨のバージョン1 UUIDについて説明しており、ブログ投稿の10年前の1998年に期限切れになったインターネットドラフト(絶対にやるべきではないこと)を引用しています。将来は懐疑的になります。私たちはずっと前にそれらの戦いを戦いました、そしてそれらのほとんどに勝ったようです。私はまだ暗号ランダムソースにきれいなAPI呼び出しを使用すると、はるかに優れていることに同意するが、それほど難しいGUIDのことはありません/バージョン4でのUUID
nealmcb

1
何の価値があるのか​​、これは「パスワードをリセットする方法」という質問には答えません。GUIDについて素晴らしい点を吐きました。
レックスウィッテン

67

編集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クリックします。

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


1
実際のユーザーパスワードがハッシュされているのに、なぜ新しいHASHキーを生成するのか疑問に思いました。ハッシュ化されたパスワードを渡してパスワードをリセットするためのリンクを記載した電子メールをユーザーに送信するのは正しくありませんか?ハッシュ化されたパスワードを元に戻すことはできません。ユーザーがリンクをクリックすると、サーバーはハッシュ化されたパスワードを受け取り、実際に保存されているものと比較して、ユーザーがパスワードを変更できるようにします。
ダニエル

また、もう1つの非常に良い点は、タイムアウトを設定する必要がないことです。ユーザーがパスワードを変更すると、データベースに保存されているハッシュパスワードが変更されたため、古いリンクは自動的に無効になります。
ダニエル

@ダニエルそれは本当に悪い考えです。「ブルートフォース攻撃」という用語をグーグルで検索する必要があると思います。また、期限切れにしたい理由は、誰かの電子メールが1年後に侵害された場合(そして、彼らがそれをリセットすることは決してない場合)、ハッカーはパスワードを変更する権利を取得するためです。
eduncan911 2015

@ educan911。私はブルートフォース攻撃を知っていますが、ハッシュキーにアクセスするには、悪意のある人が電子メールにアクセスできる必要があります。彼がそれにアクセスできる場合は、ハッシュパスワードを元に戻す必要はありません。また、ほとんど不可能にするために、ハッシュされたパスワードをハッシュするか、さらに良いことに、パスワードをもっと何かでハッシュすることができます。私はあなたに反対していません、私はそれについてブレインストーミングをしようとしているだけです
ダニエル

8

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

ユーザー名、パスワード、および作業用の電子メールアドレスがあるとすると、ユーザーテーブルに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)

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

親愛なるまあまあ

誰かが<あなたのウェブサイト名>のユーザーアカウント<ユーザー名>の新しいパスワードを要求しました。このパスワードのリセットをリクエストした場合は、次のリンクをたどってください。

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

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

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

ありがとう、やだやだ

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

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

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

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

あなたの質問に関して:

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

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

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

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

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


潜在的な攻撃者は、現在の日付スタンプのMD5を使用して侵入することはできませんか?
ジョージストッカー

電子メールでパスワードを送信しないことを強くお勧めします。ほとんどのユーザーは、これらの電子メールを削除せずに残しますが、これはセキュリティ違反です。一部のユーザーは、「お気に入り」の電子メールから毎回コピーして貼り付けたいと考えています。ユーザーの会社のメールサーバーの証明書が期限切れになり、トラフィックが盗聴された場合はどうなりますか?この可能性のある違反を最小限に抑えるには、(1)この特定のパスワードの有効期限を1時間に設定し、(2)ユーザーに次回のログオン時にパスワードを更新するように強制します。
Ognyan Dimitrov 2014

Ognyan、電子メールで送信されたパスワードは1回だけ機能します。ログイン後にパスワードを変更する必要があり、電子メールにはユーザーのログイン名が含まれていません。したがって、毎回コピーして貼り付けることはできません。電子メールを削除しないことはセキュリティの問題ではありません。パスワードがリセットされた後、攻撃者に何も得られないのは意味のない文字/数字の文字列だからです。
jmucchiello 2014

3

レコードの電子メールアドレスに送信されるGUIDは、ほとんどの一般的なアプリケーションには十分である可能性が高く、タイムアウトはさらに優れています。

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


2

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


詳細が役立つでしょう。
ジョージストッカー

2

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


0

ASP.NETIdentityに関する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; }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.