WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

MVC5 アプリに 2 要素認証を実装

by WebSurfer 2021年10月27日 12:34

Visual Studio 2019 のテンプレートを使って作成した ASP.NET MVC5 アプリには、SMS と Email を利用した 2 要素認証を使用するためのコードが組み込まれています。今更ながらですが、その機能を検証してみました。その際に備忘録として残しておくのが良さそうと思った点を以下に書いておきます。

SQL Server のユーザー情報

まず最初に、ASP.NET MVC5 が何をベースに 2 要素認証を行っているかを書きます。これを覚えておくと、この先の話が分かりやすいかと思いますので。

上の画像を見てください。これは ASP.NET Identity が使う SQL Server データベースの AspNetUsers テーブルで、この例ではアプリを実行して登録した 2 人のユーザー情報が含まれています。

2 要素認証はユーザーごとに有効 / 無効の設定ができます。有効に設定すると当該ユーザーの TwoFactorEnabled 列が True になります。

有効にしただけでは 2 要素認証は働きません。さらに条件があって、メールアドレスと SMS 用の電話番号がそれぞれ Email 列 と PhoneNumber 列に登録済みで(片方のみでも可)、その有効性が確認されて EmailConfirmed 列と PhoneNumberConfirmed 列が True になっている必要があります。

上の画像の2 番目のユーザー (Id が 7c...) がその条件を満たしています。このユーザーはログイン画面で id とパスワードを入力するだけではログインできず、その後 2 要素認証用トークンの確認画面にリダイレクトされます。別途 SMS または Email でトークンが送信されてきますので、それを確認画面に入力するとログインできます。

上の画像の 1 番目のユーザー (Id が 30...) も Manage/Index 画面で 2 要素認証を有効に(TwoTactorEnabled 列を True に)設定することはできます。しかし、EmailConfirmed 列と PhoneNumberConfirmed 列が False となっている場合は 2 要素認証は働かず、ログイン画面で id とパスワードを入力するだけでログインできてしまいます。

Email による 2 要素認証を可能にするためには AspNetUsers テーブルの Email 列に有効なメールアドレスを登録し、EmailConfirmed 列を True にする必要があります。通常それは、先の記事「Email Confirmation の実装 (MVC5)」で書いた機能で、ユーザー登録の際に自動的に設定されます。

ASP.NET Identity 2.0 ではデフォルトで id にメールアドレスを使っていますので、ユーザー登録するだけで AspNetUsers テーブルの Email 列にはメールアドレスが登録されますが、EmailConfirmed 列は False のままです。Email Confirmation 機能によりメールを受け取って、その本文にあるリンクをクリックすることにより、EmailConfirmed 列が True に設定されます。

SMS による 2 要素認証を可能にするには、PhoneNumber 列への SMS 送信先の電話番号の登録が必要になります。その操作はユーザー登録後に Manage/Index 画面にて行います。電話番号を登録すると SMS に確認用トークンが送信されてくるので、それを入力して確認することにより PhoneNumberConfirmed 列が Ture に設定されます。(詳細後述)


2 要素認証の実装方法は Microsoft のチュートリアル「SMS や電子メールで 2 要素認証する ASP.NET MVC 5 アプリ」に書かれています。

ただし、チュートリアルの手順では実際に SMS と Email の送信機能を実装しないと検証ができないのが難点です。Email Confirmation 用に SendGrid にユーザー登録して ApiKey を取得し、メールを送信できるようにするのは相当面倒でした。SMS の方は試してないのでどのぐらい面倒かは分かりませんが、チュートリアルにある Twilio のリンクをクリックしただけでその先に進んでユーザー登録しようという気力が失われました。(汗)

実際に SMS と Email でトークンを受け取ることができるのを確認しないと意味がないと言われるかもしれませんが、SMS と Email の送信機能を実装するところで挫折して先に進めないかもしれません。とりあえず基本を把握しておいて、後で実際に運用に使うプロバイダが決まってから Email や SMS の送信機能を実装して検証する方が良さそうだと思いました。

という訳で、Visual Studio 2019 のテンプレートを使って作成したプロジェクトをベースに SMS と Email の送信機能は実装しないでやってみました。

まずプロジェクトの作成ですが、チュートリアルと同様に .NET Framework 版の ASP.NET MVC5 アプリのプロジェクトを、認証に「個別のユーザーアカウント」 を選択して作成します。Core 版と間違えないよう注意してください。(下の画像は Visual Studio 2019 のもので、チュートリアルのものとは異なります)

プロパティの作成

プロジェクト作成の後、チュートリアルには書いてありませんが、ASP.NET Identity が使うユーザー情報のストアとしての SQL Server データベースの生成と、ユーザー登録が必要です。アプリを実行してブラウザ上でユーザー登録を行えば、EF Code First の機能を使ってのデータベースの作成と、データベースへのユーザー情報の登録が自動的に行われます。

ユーザー登録に成功してログイン状態になると、画面の右上に「ようこそ・・・」と表示されます。それをクリックすると以下のような Manage/Index 画面に遷移します。

Manage/Index 画面

上の画像では「2 要素認証: 2 要素認証プロバイダーが構成されていません・・・」と表示されています。それを変更して 2 要素認証の設定ができるようにします。そのためには、チュートリアルの「7.Views\Manage\Index.cshtml Razor ビューを更新します」にあるように Views\Manage\Index.cshtml に手を加えます。(手順 1 ~ 6 は SMS 送信機能の実装ですので今回はスキップ)

Visual Studio 2019 のテンプレートで作成した場合コードは生成済みでコメントアウトされていますので、それを以下のようにすれば OK です。だたし、テンプレートで生成されたコードには &nbsp;&nbsp;]</TEXT> が欠落していて、そのままではエラーになるので、下の画像の赤枠の部分のコードを追加してください。

Index.cshtml Razor ビューを更新

上の Views\Manage\Index.cshtml の更新後、Manage/Index 画面を表示すると以下のようになります。

Razor ビュー更新後の Manage/Index 画面

上の画像の「電話番号」というのは SMS を利用した 2 要素認証を行う場合の SMS 送信先です。登録してない場合は Add と言うリンクが表示されますので、それをクリックすると Manage/AddPhoneMumber に遷移します。

SMS 用の電話番号の登録

そこで SMS 送信先の電話番号を入力して[送信]ボタンをクリックすると、テンプレートで実装された Manage/AddPhoneNumber アクションメソッドのコードがトークンを生成して SmsService クラスの SendAsync メソッドを実行し、SMS を使って登録した電話番号にトークンを送信します。

この記事では SMS 送信用のコードは実装してませんので何も起こりませんが、以下のようにデバッガを使えばトークンが message の Body に含まれているのが確認できます。

SMS でトークン送信

SMS 送信後、確認画面 Manage/VerifyPhoneNumber にリダイレクトされます。

確認画面でトークンの入力

SMS で送られてきたトークンを上の確認画面の[コード]欄に入力して[送信]ボタンをクリックすると Manage/Index にリダイレクトされ、以下のように登録された SMS 用の電話番号が表示されます。

登録された SMS 用の電話番号

同時に、入力した電話番号は SQL Server データベースの AspNetUsers テーブルの当該ユーザーのレコードの PhoneNumber に登録され、PhoneNumberConfirmed が true になります。

この記事のように SMS の送信機能を実装しない場合、上の画像のようにデバッガでトークンを確認してから確認画面に入力するのは面倒ですよね。そこを何とかしたいという場合は、トークンの生成のコードは確認画面用の Manage/VerifyPhoneNumber アクションメソッドにもありますので、便宜的に(あくまで便宜的にです)以下のようにして ViewBag を使って確認画面に表示することができます。

// GET: /Manage/VerifyPhoneNumber
public async Task<ActionResult> VerifyPhoneNumber(string phoneNumber)
{
    var code = await UserManager
                     .GenerateChangePhoneNumberTokenAsync(
                         User.Identity.GetUserId(), phoneNumber);

    // SMS は使えないので ViewBag を使って View に表示されるようにした
    ViewBag.Token = code;

    // 電話番号を確認するために SMS プロバイダー経由で SMS を送信します。
    return phoneNumber == null ? View("Error") : 
        View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber });
}

以上は SMS 送信先の電話番号を登録するだけの設定です。以下に、2 要素認証を行うために、登録した SMS 用の電話番号または Email アドレス宛にトークンを送るための設定を書きます。

・・・と言っても、そのためのコードは Visual Studio 2019 のテンプレートで作成したプロジェクトにほぼ実装済みで、自力でコードを書いて実装しなければならないのは Email, SMS が送信できるようにするための機能だけです。

それらの機能は IdentityConfig.cs の EmailService クラスと SmsService クラスの中の SendAsync メソッドに実装しますが、上に書きましたようにこの記事ではとりあえずそこはスキップします。

2 要素認証プロバイダーは、SMS と Email 用については自動生成された IdentityConfig.cs の ApplicationUserManager クラスの Create メソッドで manager.RegisterTwoFactorProvider メソッドを使って以下のように登録済みです。EmailService クラスと SmsService クラスもインスタンス化されて ApplicationUserManager に設定されています。

manager.RegisterTwoFactorProvider("電話コード", 
    new PhoneNumberTokenProvider<ApplicationUser>
{
    MessageFormat = "あなたのセキュリティ コードは {0} です。"
});

manager.RegisterTwoFactorProvider("電子メール コード", 
    new EmailTokenProvider<ApplicationUser>
{
    Subject = "セキュリティ コード",
    BodyFormat = "あなたのセキュリティ コードは {0} です。"
});

manager.EmailService = new EmailService();
manager.SmsService = new SmsService();

2 要素認証の有効 / 無効はユーザーごとに設定します。Manage/Index 画面で[2 要素認証:]の[有効化]をクリックすると、SQL Server データベースの AspNetUsers テーブルの当該ユーザーの TwoFactorEnabled 列が True に変わります。

この設定により、ログインの際に SignInManager.PasswordSignInAsync メソッドの戻り値が RequiresVerification になって Account/SendCode にリダイレクトされるようになります。

この先に進んで Email による 2 要素認証が動くようにするには、この記事の一番上の画像にあるように当該ユーザーのレコードの EmailConfirmed 列が True になっていなければなりません。先の記事「Email Confirmation の実装 (MVC5)」で書きました機能が実装されていてユーザー登録の際 Email Confirmation が行われていれば True になっているはずです。Email Confirmation 機能が実装されてなければ、Visual Studio の「SQL Server オブジェクトエクスプローラー」で AspNetUsers テーブルを開いて手動で EmailConfirmed 列を True に設定してください。

それで以下の画像のように Account/SendCode にリダイレクトされた時にドロップダウンリストに[電話コード]と[電子メール コード]という選択肢が表示されるようになります。 (EmailConfirmed 列が False のままですとドロップダウンリストには[電話コード]しか表示されません)

Account/SendCode

ドロップダウンリストの[電話コード]が SMS 用で[電子メール コード]が Email 用です。分かり難い日本語ですが、それらの文字列は上のコードのとおり IdentityConfig.cs の ApplicationUserManager クラスで登録したときのもので、任意の文字列に変更可能です。

ドロップダウンリストの[電話コード]を選択して[送信]ボタンをクリックすると SMS で、[電子メール コード]を選択すると Email でトークンが送信されます。その操作は POST 側の Account/SendCode アクションメソッドの SignInManager.SendTwoFactorCodeAsync メソッドで行われます。SMS を使うか Email を使うかの切り替えは、ドロップダウンリストの選択に応じて、メソッド内部で自動的に行われます。

トークンの送信後、下の画像のように Account/VerifyCode にリダイレクトされます。赤枠で囲った部分は検証中に便宜を図るため自分が追加したもので、元のコードには含まれていません。

Account/VerifyCode

Account/VerifyCode 画面の[コード]テキストボックスに SMS または Email で送信されてきたトークンを入力して[送信]ボタンをクリックするとログインできます。


ついでに、本題とは関係ない話ですが、検証の際の便宜を図るために (いちいちデバッガで SendAsync メソッドの引数に含まれるトークンを調べなくても済むようにするため)、上の画像の赤枠で囲ったようにトークンを Account/VerifyCode 画面に表示する方法を書いておきます。トークンは以下のように Account/SendCode アクションメソッドで取得します。

// POST: /Account/SendCode
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> SendCode(SendCodeViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View();
    }

    // トークンを生成して送信します。
    if (!await SignInManager.SendTwoFactorCodeAsync(model.SelectedProvider))
    {
        return View("Error");
    }

    // SMS, Email は使えないのでここでトークンを取得して
    // Account/VerifyCode に渡すコードを追加
    var id = await SignInManager.GetVerifiedUserIdAsync();
    var provider = model.SelectedProvider;
    var code = await UserManager.GenerateTwoFactorTokenAsync(id, provider);

    // パラメータ token という名前で Account/VerifyCode
    // にトークンを渡す
    return RedirectToAction("VerifyCode", 
        new { 
            Provider = model.SelectedProvider, 
            ReturnUrl = model.ReturnUrl, 
            RememberMe = model.RememberMe,
            Token = code        // これを追加
        });
}

上のコードの最後で Accout/VerifyCode にリダイレクトしていますが、そこで取得したトークンを Token という名前のパラメータとして設定して渡します。Accout/VerifyCode アクションメソッドの引数でパラメータ(クエリ文字列)Token に設定されたトークンを取得し、ViewBag を使って View に渡して表示しています。

Tags: , , ,

MVC

ASP.NET Identity にエンティティ追加

by WebSurfer 2021年6月23日 14:54

Visual Studio のテンプレートで認証を「個別のユーザーアカウント」としてプロジェクトを作成すると ASP.NET Identity を使った認証システムが実装されます。それにエンティティを追加する方法を書きます。この記事の例では下の画像の左上の Post エンティティがそれです。

ASP.NET Identity に Post エンティティを追加

Post というのは投稿という意味です。複数のユーザーが複数の投稿をするアプリで、投稿内容を SQL Server データベースで保持し、Post エンティティクラスを定義して Entity Framework 経由で書き込み、読み出し、編集などを行うという想定です。

ユーザーの誰が投稿を書いたかを識別できるように、上の画像のように Post と ApplicationUser をナビゲーションプロパティで紐づけます。(結果、EF Code First で生成されるデータベースでは、ApplicationUser に該当する dbo.AspNetUsers テーブルの Id 列に、dbo.Posts テーブルの ApplicationUserId 列から FK 制約が張られます)

上の画像の Post エンティティ以外の基本のエンティティクラスとコンテキストクラスは ASP.NET Identity の中で定義済みです。(.NET Framework 版の場合は Models/IdentityModels.cs に ApplicationUser クラスと ApplicationDbContext クラスが含まれていて、その継承元で定義されています)

それに Post エンティティを追加してナビゲーションプロパティを張るには Post クラスを追加するだけではダメで、既存の基本のエンティティクラスとコンテキストクラスにも手を加える必要があります。それをどのようにするかというのがこの記事の話です。

.NET Framework 版の場合は自動生成された Models/IdentityModels.cs にコンテキストクラスとエンティティクラスが定義されていますので、それに手を加えて Migration を実行することになります。

手を加えた Models/IdentityModels.cs のサンプルコードは以下の通りです。コードの中で「追加」とコメントした部分を追加しているだけです。(Core 版は場所が違うので注意。この記事の下の方の説明と画像を見てください)

using System.Data.Entity;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;

// 追加
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Mvc5App3.Models
{
    // ApplicationUser クラスにさらにプロパティを追加すると、ユーザーの
    // プロファイル データを追加できます。詳細については、
    // https://go.microsoft.com/fwlink/?LinkID=317594 を参照してください。
    public class ApplicationUser : IdentityUser
    {
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
                               UserManager<ApplicationUser> manager)
        {
            // authenticationType が CookieAuthenticationOptions
            // .AuthenticationType で定義されているものと一致して
            // いる必要があります
            var userIdentity = await manager.CreateIdentityAsync(
                 this, DefaultAuthenticationTypes.ApplicationCookie);
            // ここにカスタム ユーザー クレームを追加します
            return userIdentity;
        }

        // 追加
        public virtual IList<Post> Posts { get; set; }
    }

    // 追加
    public class Post
    {
        [Key, Required]
        public int PostId { get; set; }

        [Required, MaxLength(128)]
        public string Title { get; set; }

        [Required, MaxLength(1024)]
        public string Content { get; set; }

        [Required, ForeignKey(nameof(User))]
        public string ApplicationUserId { get; set; }

        public virtual ApplicationUser User { get; set; }
    }

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }

        // 追加
        public DbSet<Post> Posts { get; set; }
    }
}

上のコードの通りコンテキストクラスとエンティティクラスに手を加えてから Migration 操作 (Add-Migration, Update-Database) を行うと以下のテーブルが SQL Server データベースに生成されます。

Migration 操作で追加された Posts テーブル

dbo.Posts テーブルの ApplicationUserId 列から ApplicationUser に相当するテーブル dbo.AspNetUsers の Id 列に FK 制約が張られます。もちろん Models/IdentityModels.cs のコードに定義したナビゲーションプロパティも期待通り働きます。


以上は .NET Framework 版の話です。Core 版の場合も上記とほぼ同じようにして既存のデータベースに Posts テーブルを追加し、既存の AspNetUsers テーブルの Id 列に FK 制約を張ることができます。

ただし、Core 版では、Visual Studio のテンプレートで「個別のユーザーアカウント」を選んでプロジェクトを作成しても、ASP.NET Core Identity 関係のソースコードは含まれないことに注意してください。Razor Class Library (RCL) として実装されますのでコンテキストクラスとエンティティクラスに手を加えることができません。

なので、Core 版でこの記事のようなことを行う場合は、認証なしでプロジェクトを作成した後でスキャフォールディング機能を使ってソースコードと共に ASP.NET Core Identity を実装するのが良さそうです。

そうした場合、コンテキストクラスとエンティティクラスは下の画像の場所に生成されます。

コンテキストクラスとエンティティクラス

上の画像ではクラス名が ApplicationDbContext, ApplicationUser となっていますが、デフォルトの設定のまま進めると Application の部分がプロジェクト名になります。スキャフォールディングを行う際に任意に設定できますので、この記事では .NET Framework 版と同じ名前に設定しています。

上の画像の ApplicationDbContext, ApplicationUser クラスに、上のサンプルコードで書いたようにナビゲーションプロパティを追加します。

Post クラスはプロジェクトの既存の Models フォルダにクラスファイルを追加してそれに定義するのが良いと思います。

Tags: , ,

MVC

Password Recovery の実装 (MVC5)

by WebSurfer 2021年3月17日 13:35

.NET Framework 版の ASP.NET MVC5 で ASP.NET Identity をユーザー認証に使用し、Password Recovery を実装する方法を備忘録として書いておきます。先の記事「Email Confirmation の実装 (MVC5)」の続きです。

ForgotPassword ページ

Microsoft のチュートリアル「ログイン、電子メール確認、パスワード リセットを使用して安全な ASP.NET MVC 5 Web アプリを作成する (C#)」の「パスワードの回復/リセット」以降のセクションの説明に従って実装します。

Password Recovery はユーザーがパスワードを忘れてしまっても再設定できる手段を提供するものです。ページのヘッダ右上に表示されている [ログイン] リンクをクリックして Account/Login ページを表示すると、その中に [パスワードを忘れた場合] というリンクがあります(注: デフォルトではコメントアウトされていますので解除してください)。それをクリックすると上の画像の Accpunt/ForgotPassword ページに遷移します。

その [電子メール] テキストボックスに登録したメールアドレスを入力して [電子メール リンク] ボタンをクリックすると、入力したメールアドレスが Account/ForgotPassword アクションメソッドに POST されます。

Account/ForgotPassword アクションメソッドでは、(1) 送信されたメールアドレスが登録済み、(2)Email Confirmation 済みであることを確認します。条件 (1), (2) が確認できない場合は Account/ForgotPasswordConfirmation ページにリダイレクトされるだけでそれ以外何も起こりません。

条件 (1), (2) の条件の両方が確認できたらメールを送信するのですが、そのためにはテンプレートで生成された POST 側の Account/ForgotPassword アクションメソッド内の code(確認用のトークン)の生成、メール本文に含める url 作成、メール送信のためのコードのコメントアウトを解除します。

修正後のコードは以下の通りです。

// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(
                                  ForgotPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindByNameAsync(model.Email);
        if (user == null || 
            !(await UserManager.IsEmailConfirmedAsync(user.Id)))
        {
            return View("ForgotPasswordConfirmation");
        }

        // code の生成、メール本文に含める Accout/ResetPassword
        // への url 作成(既存のコードのコメントアウトを解除)
        string code = await UserManager
                      .GeneratePasswordResetTokenAsync(user.Id);
        var callbackUrl = Url.Action("ResetPassword", 
                          "Account", 
                          new { userId = user.Id, code = code }, 
                          protocol: Request.Url.Scheme);

        // メール送信。既存のコードのコメントアウト解除でもよいが、
        // ユーザーのメーラーが html の表示を許可していない可能性を
        // 考えて、html の a 要素を組み立てて送るのではなく、url そ
        // のものを送信するように変更した
        await UserManager.SendEmailAsync(user.Id,
            "Reset Password",
            "Please confirm your account by: " +
            Server.HtmlEncode(callbackUrl).Replace("&amp;", "&"));

        return RedirectToAction("ForgotPasswordConfirmation", 
                                "Account");
    }

    return View(model);
}

メールが送信された後、Account/ForgotPasswordConfirmation ページにリダイレクトされ、以下のような画面が表示されます。ユーザーにメールを見るよう促しているだけです。

ForgotPasswordConfirmation ページ

上のコードで送信されたメールは以下の画像のようになります。メール本文には Account/ResetPassword ページへの url が含まれ、それに userId と code がクエリ文字列として設定されています。

Reset Password メール

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

ResetPassword ページ

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

ResetPasswordConfirmation ページ

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


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

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

Tags: , ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar