WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

Password Recovery (CORE 版)

by WebSurfer 19. May 2020 12:59

Password Recovery はユーザーがパスワードを忘れてしまっても復活できる手段を提供するものです。ASP.NET Core 3.1 の Password Recovery がデフォルトの実装ではどのような動きになるかを以下に説明します。

(先の記事「Email Confirmation の実装 (CORE 版)」の「(9) Password Recovery」の補足です)

ページのヘッダ右上に表示されている [Login] をクリックして Login ページを表示すると、その中に [Forgot your password?] というリンクがあります。それをクリックすると下の画像の ForgotPassword ページに遷移します。

ForgotPassword ページ

その Email 欄にメールアドレスを入力して [Submit] をクリックすると、ForgotPassword.cshtml.cs の OnPostAsync メソッドで (a) 送信されたメールアドレスが登録済み、(b)Email Confirmation 済みであることを確認します。

条件 (a), (b) が確認できない場合は単純に ForgotPasswordConfirmation ページにリダイレクトされるだけでそれ以外何も起こりません。

条件 (a), (b) の条件の両方が確認できるとメールが送信され、その後で ForgotPasswordConfirmation ページにリダイレクトされます。以下のような画面になります。ユーザーにメールを見るよう促しているだけです。

ForgotPasswordConfirmation ページ

送信されたメールの本文には ResetPassword ページに確認用のトークンをクエリ文字列に含めた URL が含まれます。

Reset Password メール

上の画像のメールは、ユーザーのメーラーが html の表示を許可していない可能性を考えて URL そのものを送信するようにしています。(自動生成されるコードでは、URL を a 要素の href 属性に設定したハイパーリンク "clicking here" になります)

具体的には、Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs の OnPostAsynce() メソッドの中の SendEmailAsync メソッドのコードを以下のようにしています。

await _emailSender.SendEmailAsync(
    Input.Email,
    "Reset Password",
    "Please confirm your account by: " + 
        HtmlEncoder.Default.Encode(callbackUrl));

メールの URL をクリックすると ResetPassword ページが GET 要求され、クエリ文字列から確認用のトークンが渡されます。その応答としてブラウザに以下のような画面が表示されます。

ResetPassword ページ

上のページに、最初に ForgotPassword ページで申請したのと同じメールアドレスと、新しいパスワードを入力して [Submit] をクリックすると、メールアドレスとトークンが有効であればパスワードが新しいものに変更され、その後 ResetPasswordConfirmation ページにリダイレクトされます。

ResetPasswordConfirmation ページ

そのページのリンク [click here to log in] をクリックすると Login ページに遷移するので、そこでメールアドレスと新しいパスワードを入力すればログインできます。


ユーザー登録の場合と違って、Password Recovery ではメールが受信でき、それに含まれるトークンを ResetPassword ページに送信し、そのページで新しいパスワードを登録できないと何ともならない・・・すなわちメール送信機能の実装は必須のコーディングとなっています。

メール送信機能を実装しなくても Password Recovery を可能にするには、コードを書き換えて、 ForgotPasswordConfirmation ページで上に書いた条件 (a), (b) の条件の両方が確認できたら、ResetPassword ページにリダイレクトするといった修正が必要と思われます。(未検証・未確認です)

Tags: , , ,

CORE

Email Confirmation の実装 (CORE 版)

by WebSurfer 18. May 2020 16:12

ASP.NET Core 3.1 MVC で ASP.NET Identity をユーザー認証に使用し、Email Confirmation と Password Recovery を実装する際、自分的に気になったことを備忘録として書いておきます。

(.NET Framework 版については特に気を付ける点はなさそうなので割愛します。手抜きですみません。基本的な枠組みが Visual Studio のテンプレートで生成されるコードに含まれており、Microsoft のチュートリアル「ログイン、電子メール確認、パスワード リセットを使用して安全な ASP.NET MVC 5 Web アプリを作成する (C#)」に従って生成されたコードに手を加えれば特に苦労なく実装できると思います)

基本的には Microsoft のチュートリアル Account confirmation and password recovery in ASP.NET Core に従って実装しますので、詳しくはそのドキュメントを見てください。そのドキュメントの説明だけではよく分からないと思った点を以下に書きます。

(1) プロジェクトの作成

Visual Studio 2019 のテンプレートを利用して ASP.NET Core 3.1 MVC アプリを作成します。

認証関係のソースコードに手を加えるので、アプリ作成の際に認証は「認証なし」のままとしておき、スキャフォールディング機能を利用して ASP.NET Identity を実装し、ソースコードがプロジェクトに含まれるようにします。

詳しい手順は先の記事「ASP.NET Identity で MySQL 利用 (CORE 版)」のステップ「(1) プロジェクトの作成」とステップ「(2) ASP.NET Core Identity の実装」を見てください。

(2) Email Confirmation 実装前の動き

上のステップ (1) でプロジェクトを作成直後(Email Confirmation を実装する前)のユーザー登録の仕組み説明します。それを理解しておいた方がこの後の説明が分かりやすいと思いますので。仕組みをご存じの方はここはスキップして (3) に進んでください。

デフォルトで options.SignIn.RequireConfirmedAccount が true に設定されており(そのコードは Areas/Identity/IdentityHostingStartup.cs にあります)、ユーザー登録に使用する Register ページも Email Confirmation を使用する前提でコーディングされています。ちなみに false に設定すると登録後直ちにログイン状態になります。

Areas/Identity/Pages/Account フォルダにある Register.cshtml.cs を開いて OnPostAsync メソッドのコードを見てください。登録画面にユーザーが email と password を入力して送信するとそのメソッドでユーザー登録処理が行われます。

ユーザー登録に成功すると RegisterConfirmation ページにリダイレクトされます。(実際はリダイレクト前に _emailSender.SendEmailAsync メソッドが実行されますが、デフォルトではメール送信機能は実装されていないので何も起こりません。何故かエラーも出ません)

RegisterConfirmation ページは下の画像のように表示されます。

RegisterConfirmation ページ

上の画像の "Click here to confirm your account" には ConfirmEmail ページへのリンクが張ってあり、クエリ文字列に UserId とトークンが設定されています。

リンクをクリックすると ConfirmEmail ページが GET 要求され Areas/Identity/Pages/Account フォルダにある ConfirmEmail.cshtml.cs の _userManager.ConfirmEmailAsync(user, code) メソッドで Email Confirmation が実行(AspNetUsers テーブルの EmailConfirmed フィールドが True に更新)されます。

Email Confirmation に成功すると下の画像の ConfirmEmail ページが表示されます。

ConfirmEmail ページ

この後、右上の [Login] をクリックして Login ページを表示し、登録した email と password を入力してログインできるようになります。

Email Confirmation を実装してメールを送信できるようにすると、ConfirmEmail ページへのリンク(UserId とトークンのクエリ文字列を含む)が本文に含まれたメールが自動的に送信され、それをクリックすると ConfirmEmail ページが GET 要求されて Email Confirmation が実行されるようになります。

RegisterConfirmation ページへのリダイレクトは同様に起こりますが、それには ConfirmEmail ページへのリンクは含まれず、単に "Please check your email to confirm your account." と表示されただけものになります。

以下のステップ (3) 以降にメールの自動送信を含む Email Confirmation の実装方法を述べます

(3) API Key の入手

チュートリアルに従って、メールの送信には SendGrid を利用します。ユーザー登録して API Key を入手してください。

API Key の入手

SendGrid は基本的に有償ですが、開発目的限定で送信する量が少ない場合は Free のサービスでよさそうです。Pricing のページを見てください。

(4) SendGrid のインストール

NuGet を利用してプロジェクトに SendGrid をインストールします。

SendGrid をインストール

上の画像ではバージョンが 9.12.6 となっていますが、自分が試したときはそれが最新版だったということで、意図的にそのバージョンを選んだわけではありません。

(5) User Secrets

メールの差出人と SendGrid の API Key をユーザーシークレットに設定します。チュートリアルは dotnet コマンドを使っていますが、Visual Studio のソリューションエクスプローラーから開いて設定できます。

ユーザーシークレットの管理

上の画像のように操作すると secrets.json ファイルが開くので、それに以下のようにメールの差出人と SendGrid の API Key を設定して保存します。

secrets.json

普通に appsettings.json に設定しても取得できます。機密性の高い情報を開発時にどこに保存するかのチュートリアルの意味でユーザーシークレットを紹介しているのかもしれません。

ユーザーシークレットは開発時の開発マシンのでの利用に限られます。secrets.json ファイルは開発マシンのローカルフォルダにあるので当たり前ですが・・・ 詳しくは Safe storage of app secrets in development in ASP.NET Core を見てください。

チュートリアルには AuthMessageSenderOptions クラスの追加と Startup.ConfigureServices へのコードの追加を行うように書かれています。

まず、プロジェクトルート直下に Services フォルダを追加し、その中にクラスファイル AuthMesageSenderOptions.cs を追加します。コードはチュートリアルのものをコピペします。

// AuthMessageSenderOptions クラスの追加

namespace MySQLIdentity.Services
{
    public class AuthMessageSenderOptions
    {
        public string SendGridUser { get; set; }
        public string SendGridKey { get; set; }
    }
}

次に、Startup.cs の ConfigureServices メソッド内に以下のコードを追加します。

// Startup,cs の ConfigureServices メソッド内に追加

services.Configure<AuthMessageSenderOptions>(Configuration);

これらは、AuthMessageSenderOption クラスを、必要に応じて初期化してそのオブジェクトを DI によって注入するために必要なもののようです。

その設定により、secrets.json ファイルに設定した差出人と API Key 情報は、EmailSender クラスを初期化する際に DI によって注入された AuthMessageSenderOption オブジェクトから取得できます。

なお、secrets.json ファイルを使わなくても、普通に appsettings.json に差出人と API Key 情報を設定しても同様に DI によって取得できます(secrets.json と appsettings.json の両方から探してくるようです)。運用環境にデプロイする際に appsettings.json に移すということではなさそうですが、実際どうすべきかは調べておらず不明です。

(6) EmailSender クラスの実装

メールを送信するため IEmailSender インターフェイスを継承した EmailSender クラスを実装します。

チュートリアルに従ってクラスファイル Services/EmailSender.cs を追加し、それにチュートリアルのコードをそのままコピペしてください。

念のため、以下にコードを貼っておきます。

using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;
using System.Threading.Tasks;

namespace MySQLIdentity.Services
{
    public class EmailSender : IEmailSender
    {
        public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor)
        {
            Options = optionsAccessor.Value;
        }

        public AuthMessageSenderOptions Options { get; }

        public Task SendEmailAsync(string email, string subject, string message)
        {
            return Execute(Options.SendGridKey, subject, message, email);
        }

        public Task Execute(string apiKey, string subject, string message, string email)
        {
            var client = new SendGridClient(apiKey);
            var msg = new SendGridMessage()
            {
                From = new EmailAddress("xxxxx", Options.SendGridUser),
                Subject = subject,
                PlainTextContent = message,
                HtmlContent = message
            };
            msg.AddTo(new EmailAddress(email));

            // Disable click tracking.
            // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
            msg.SetClickTracking(false, false);

            return client.SendEmailAsync(msg);
        }
    }
}

上のコード From = new EmailAddress("xxxxx", Options.SendGridUser) の xxxxx をメールの差出人のメールアドレスに変更してください。

EmailSender クラスが初期化される際に DI により AuthMessageSenderOptions オブジェクトが注入され、差出人と API Key 情報が取得できるようにコーディングされています。

EmailSender クラスは Register ページと ForgotPassword ページでメールを送信する際に使用されます。その際、DI により EmailSender クラスを初期化してそのオブジェクトを注入するコーディングになっています。それを可能にするため、Startup.cs の ConfigureServices メソッド内に以下のコードを追加します。

// Startup.cs の ConfigureServices メソッド内に追加

services.AddTransient<IEmailSender, EmailSender>();

上記の Startup.cs への設定を忘れるとメール送信機能は動きませんので注意してください。

(7) Register ページの修正

Areas/Identity/Pages/Account フォルダの Register.cshtml.cs を開いて OnPostAsync メソッドのコードを見てください。

ユーザー登録に成功すると変数 callbackUrl に UserId とトークン情報がクエリ文字列として設定された /Account/ConfirmEmail ページへの URL 文字列が設定されます。

デフォルトでは html の a 要素の href 属性にその URL が設定されたメール本文がユーザーが email として登録したメールアドレスに送信されます。

ユーザーが使っているメーラーが html を表示できる設定になっていれば、その a 要素で "clicking here" と表示されるリンクをクリックすれば ConfirmEmail ページが GET 要求され、クエリ文字列に設定された UserId とトークンで Email Confirmation が行われ、ブラウザが立ち上がって ConfirmEmail ページが表示されます。

ユーザーのメーラーが html の表示を許可していない可能性を考えて、html の a 要素を組み立てて送るのではなく、URL そのものを送信したい場合は以下のようにします。

await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
    "Please confirm your account by: " + 
    HtmlEncoder.Default.Encode(callbackUrl).Replace("&amp;", "&"));

その際、その際クエリ文字列内の &amp; は & にしないと ConfirmEmail の OnGetAsync(string userId, string code) の code が null になってしまうので注意してください。

上記の結果、送信されたメールは以下の画像のようになります。(メーラーは Windows Live メールです)

送信されたメール

(8) RegisterConfirmation ページの修正

Areas/Identity/Pages/Account フォルダの RegisterConfirmation.cshtml.cs を開いて OnGetAsync メソッドを以下のように修正します。

RegisterConfirmation ページの修正

Email Confirmation を実装したので ConfirmationEmail ページへのリンクを表示する必要はありません。そのリンクを生成するコードをコメントアウトします。

加えて DisplayConfirmAccountLink を falses に設定し、ConfirmationEmail ページへのリンクではなくて単に "Please check your email to confirm your account." と表示されるようにします。

(9) Password Recovery

Password Recovery はユーザーがパスワードを忘れてしまっても復活できる手段を提供するものです。

上のステップ「(6) EmailSender クラスの実装」 まででメールは送信されるようになっていますので、基本的な機能だけなら既存のコードに手を加える必要はありません。

なお、スキャフォールディングで実装されるコードはメール送信機能を実装しないと Password Recovery が使えないコーディングになっていますので注意してください。(メール送信機能なしで Password Recovery を利用するにはコードを書き換える必要があります)

以下に Password Recovery がどのような動きになるかを説明しようと思いましたが、長くなりすぎるので別の記事「Password Recovery (CORE 版)」に書きました。興味があればそちらを見てください。

Tags: , , ,

CORE

ASP.NET Core 3.1 Web API

by WebSurfer 15. May 2020 12:45

ASP.NET Web API の Core 3.1 版のアプリが .NET Framework 版のアプリと異なる点を備忘録としてまとめておきます。あくまで自分的に大きな違いがあると思っている点のみです。

  1. Controller は ControllerBase クラスを継承する。(.NET Framework の ApiController ではなく)
  2. Controller に ApiControllerAttribute 属性を付与する。
  3. ルーティングは Controller に RouteAttibute 属性を付与して設定する。(.NET Framework 版プロジェクトで生成される "api/{controller}/{id}" というようなルーティング設定はないようです)
  4. アクションメソッドに [HttpGet], [HttpGet("{id}")], [HttpPost] 等の属性を付与する。(付与しないと HTTP 動詞でどれを呼ぶかの判別ができないようです)
  5. アクションメソッドの引数、例えば [FromBody] string name の name に文字列をバインドする場合、.NET Framework 版では「=文字列」という文字列をコンテンツとして送信すれば可能でしたが、Core 版ではそれができない。
  6. .NET のクラスのプロパティ名の最初の文字は大文字にするのが普通ですが、大文字にしておいてもシリアライズされた結果の応答の JSON 文字列のキー名の最初の文字が小文字になる。

詳しくは Microsoft のドキュメント「ASP.NET Core を使って Web API を作成する」に書いてありますのでそちらを見てください。

コード例を示すと以下のようになります。先の記事「ASP.NET Web API のバインディング」に .NET Framework 版のコードがありますので、それと比較してみてください。

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using WebAPI.Models;

namespace WebAPI.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private List<Hero> heroes = new List<Hero> {
              new Hero {Id = 1, Name = "スーパーマン"},
              new Hero {Id = 2, Name = "バットマン"},
              new Hero {Id = 3, Name = "ウェブマトリクスマン"},
              new Hero {Id = 4, Name = "チャッカマン"},
              new Hero {Id = 5, Name = "スライムマン"}
          };

        // GET api/values (Read...すべてのレコードを取得)
        [HttpGet]
        public List<Hero> Get()
        {
            return heroes;
        }

        // GET api/values/5 (Read...id 指定のレコード取得)
        [HttpGet("{id}")]
        public Hero Get(int id)
        {
            return heroes[id - 1];
        }

        // POST api/values  (Create...レコード追加)
        [HttpPost]
        public List<Hero> Post(Hero postedHero)
        {
            heroes.Add(postedHero);
            return heroes;
        }

        // PUT api/values/5 (Update...id 指定のレコード更新)
        [HttpPut("{id}")]
        public List<Hero> Put(int id, Hero postedHero)
        {
            heroes[id - 1].Name = postedHero.Name;
            return heroes;
        }

        // DELETE api/values/5 (Delete...id 指定のレコード削除)
        [HttpDelete("{id}")]
        public List<Hero> Delete(int id)
        {
            heroes.RemoveAt(id - 1);
            return heroes;
        }
    }
}

上に列挙した 5 番目の項目に書いた件を説明します。上のコード例の Put の場合を見てください。.NET Framework 版の記事ではアクションメソッドの引数を Put(int id, [FromBody] string name) とし、それを呼び出す jQuery ajax のコードでは data: encodeURI("=ガッチャマン") として無理やり(?)引数 name にバインドできていました。Core 版ではそれはできないようで、以下のコードのようにしないとダメでした。

// .NET Framework ベースとは異なり、以下のようにしないとダメ
function apiHeroesPut5() {
    var j = { Id: 5, Name: "ガッチャマン" };
    var jsonString = JSON.stringify(j);
    $.ajax({
        type: "PUT",
        url: "/values/5",
        data: jsonString,
        contentType: "application/json; charset=utf-8",
        success: function (data) {
            $('#heroes').empty();
            $.each(data, function (key, val) {
                var str = val.id + ': ' + val.name;
                $('<li/>', { html: str }).appendTo($('#heroes'));
            });
        },
        error: function (jqXHR, textStatus, errorThrown) {
            $('#heroes').empty();
            $('#heroes').text('textStatus: ' + textStatus +
                ', errorThrown: ' + errorThrown);
        }
    });
}

理由は不明です。バインディング ソース パラメーター推論を読むと分かるかも。手抜きですみません。

最後に、上に列挙した 6 番目の項目に書いた、シリアライズされた結果の応答の JSON 文字列のキー名の最初の文字が小文字になる件を説明します。

上の 1 番目のコード例を見てください。Hero クラスのプロパティ名 Id, Name の最初の文字は大文字にしています。しかし、Web API が Hero オブジェクトを JSON 文字列にシリアライズすると何故かキー名は id, name というように小文字になります。

2 番目のコード例の success: function (data) の data には応答の JSON 文字列をデシリアライズした JavaScript オブジェクトが渡されますが、キー名から値を取得する際、最初の文字を小文字にしないと値を取得できず undefined になってしまいます。(なので上のコードでは val.id, val.name としています)

何を隠そう自分はここにハマって 1 ~ 2 時間悩みました。(笑)

Tags: ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  September 2020  >>
MoTuWeThFrSaSu
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar