WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ASP.NET Core アプリに 2 要素認証を実装

by WebSurfer 6. November 2021 12:32

Visual Studio 2019 のテンプレートで作成した ASP.NET Core 3.1 以降のアプリには TOTP ベースの認証アプリを用いて 2 要素認証を行うコードが実装されています。なので、開発の際プログラマは何も実装しなくても、Google Authenticator App などの認証アプリを用いた 2 要素認証を有効にできます。

EnableAuthenticator 画面

実は、最初、自分は Microsoft のドキュメント「ASP.NET Core での多要素認証」にいろいろ書いてあるのを見て惑わされて、一体何をどのように実装すればいいのか分からなかったです。(汗)

実際に試してみると、そのドキュメントの「MFA TOTP (時間ベースのワンタイム パスワード アルゴリズム)」セクションの 2 要素認証のコードは実装済みで、何も手を加える必要はなかったです。

唯一、認証アプリに秘密キーを共有させる画面(Manage/EnableAuthenticator 画面)には QR コードが表示されないのが難点でしたが、そこは別のドキュメント「ASP.NET Core での TOTP authenticator アプリの QR コード生成を有効にする」に qrcode.js という JavaScript ライブラリを使って表示できるようにする方法が書いてありました。

この記事の一番上の画像が、そのドキュメントに従って QR コードを表示できるようにした Manage/EnableAuthenticator 画面です。そうするだけで、テンプレートで実装済みの 2 要素認証は十分実用になると思います。

という訳で、実際にいろいろやってみましたので、その結果を備忘録として残しておきます。

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

Visual Studio 2019 16.11.5 のテンプレートで、認証は「なし」にして .NET 5.0 の ASP.NET Core MVC アプリのプロジェクトを作成しました。

その後、スキャフォールディング機能を利用して ASP.NET Core Identity 関係のソースコードを一式全て実装します。(認証を「個別のユーザー」にすると、認証関係の機能は Razor Class Library (RCL) として提供され、ソースコードはプロジェクトには含まれません。後で必要な部分のみソースコードに差し替えることができますが、いろいろ面倒です)

(2) ユーザーの登録

新規ユーザーとして oz@mail.example.com (以下、oz と書きます) というユーザーを登録します。

ユーザー登録しただけの状態ですと、ASP.NET Core Identity が使う SQL Server データベースの AspNetUsers テーブルの当該ユーザーの TwoFactorEnabled 列は False となっています。

AspNetUsers テーブル

TOTP 用の秘密キー等は AspNetUserTokens テーブルに格納されますが、この時点ではユーザー oz のデータはありません。

(2) TOTP 秘密キーの生成

ログインして Manage/Index 画面に進み、[Two-factor authentication]をクリックして Manage/TwoFactorAuthentication 画面を表示して、そこで [Add authenticator app]をクリックします。

TwoFactorAuthentication 画面

この記事の一番上の画像の Manage/EnableAuthenticator 画面に遷移しますので、そこで認証アプリに秘密キーを渡すことができます。

ユーザー oz の TOTP 用の秘密キーが生成され、SQL Server データベースの AspNetUserTokens テーブルに格納されます。

AspNetUserTokens テーブル

ちなみに、もしここで認証アプリには秘密キーは渡さずログオフした場合、次回ログインして Manage/TwoFactorAuthentication 画面を表示すると上の Manage/TwoFactorAuthentication 画面とは違って以下のようになります。

TwoFactorAuthentication 画面

[Setup authnticator app]をクリックすると、この記事の一番上の画像の Manage/EnableAuthenticator 画面に遷移します。

[Reset authentication app] をクリックすると Manage/ResetAuthenticator 画面に遷移します。

ResetAuthenticator 画面

上の画面で[Reset authenticator key]をクリックすると、秘密キーが変更されてから、この記事の一番上の画像の Manage/EnableAuthenticator 画面に遷移します。

(3) TOTP 秘密キーの共有

この記事の一番上の画像の Manage/EnableAuthenticator 画面に表示された QR コードを認証アプリでスキャンすると 6 桁の数字のパスワードをが認証アプリに表示されますので、それを[Verification Code]欄に入力し[verify]ボタンをクリックすると以下の Manage/ShowRecoveryCodes 画面に遷移します。

ShowRecoveryCodes 画面

認証アプリをインストールしたデバイスを紛失するとアクセスできなくなるので、それに備えてリカバリーコードは記録しておいた方が良いとのことです。リカバリーコードは秘密キーと共に AspNetUserTokens テーブルに保存されます。

AspNetUserTokens テーブル

以上の操作により、ASP.NET Core アプリと認証アプリで秘密キーが共有され、AspNetUsers テーブルの TwoFactorEnabled は True に設定されて、認証アプリを利用しての 2 要素認証が可能になります。

(4) 2 要素認証でのログイン

一旦ログアウトしてから、まずログイン画面でのメールとパスワードによるログイン操作を行います。成功すると以下の Account/LoginWith2fa 画面に遷移しますので、認証アプリを起動してパスワードを取得し、[Authenticator code]欄に入力して[Log in]ボタンをクリックすれば 2 要素認証機能が働いでログインできます。

LoginWith2fa 画面

(5) その他

2 要素認証が有効になった後 Manage/TwoFactorAuthentication 画面を表示すると以下のようになります。

TwoFactorAuthentication 画面

そこで[Disable 2FA]ボタンをクリックすると Manage/Disable2fa 画面に遷移します。

Disable2fa 画面

上の画面で[Disable2fa]ボタンをクリックすると AspNetUsers テーブルの当該ユーザーの TwoFactorEnabled 列は False となり、ログイン画面でメールとパスワードでログイン操作を行うだけでログインできるようになります(2 要素認証は無効になります)。

再度 2 要素認証を有効にするには、ログイン画面でメールとパスワードでログイン操作を行ってログインしてから Manage/TwoFactorAuthentication 画面に進み、[Setup authenticator app]ボタンをクリックして Manage/EnableAuthenticator 画面を表示し、上のステップ (3) と同じ操作を行います。

[Reset recovery codes]ボタンをクリックすると Manage/GenerateRecoveryCodes 画面に遷移します。

GenerateRecoveryCodes 画面

そこで[Generate Recovery Codes]ボタンをクリックすると、上のステップ (3) にあるのと同様な Manage/ShowRecoveryCodes 画面に遷移し、再発行されたリカバリーコードが表示されます。同時に AspNetUserTokens テーブルの既存のリカバリーコードは画面に表示されたものに書き換えられます。


なお、MVC5 ではテンプレートに標準で実装されていた SMS を用いた 2 要素認証は非推奨だそうです。上に紹介した記事には "SMS を使用する方法は推奨されなくなりました。 この種の実装には、既知の攻撃ベクトルが多すぎます" と書いてあります。

Core 1.1 用には「SMS を使用した 2 要素認証 (ASP.NET Core)」というチュートリアルがありますが、.NET Core 3.1 とか .NET 5.0 の新し��バージョンのテンプレートで作ったプロジェクトにはそのチュートリアルのコードは実装されていません。

Tags: , , , , , ,

CORE

MVC5 アプリで Google Authenticator による 2FA

by WebSurfer 4. November 2021 15:27

先の記事「MVC5 アプリに 2 要素認証を実装」で SMS と Email を利用した 2 要素認証の実装に関する記事を書きました。この記事にはそれに認証アプリ Google Authenticator App を利用した 2 要素認証を追加する話を書きます。

Google Authenticator

認証アプリは Google Authenticator App でなければならないという訳ではなく、TOTP (Time-based One-time Password Algorithm) ベースの認証アプリであれば Microsoft Authenticator App なども使えるそうですが、今回は Google Authenticator App を使ってみました。

まず、TOTP というのがどういう仕組みかですが、ITmedia NEWS の記事「SMS認証の仕組みと危険性、「TOTP」とは? 「所有物認証」のハナシ」が分かりやすいと思いますので見てください。リンク切れになると困るので図だけ借用して下に貼っておきます。

Time-based One-time Password Algorithm

今回の ASP.NET MVC5 アプリのケースで説明します。まず、ASP.NET MVC5 アプリ(上の図で「認証サーバ」と書いてあるもの)が発行する秘密キーを Google Authenticator App(上の図で「ソフトウェアトークン」と書いてあるもの)に取得させ共有します。秘密キーは ASP.NET MVC5 アプリに登録されたユーザー毎に異なる 32 文字の文字列です。Google Authenticator App は、そのユーザーが保有するスマートフォンにインストールされていることが前提です。

この記事の一番上にある画像は ASP.NET MVC5 アプリが表示した画面で、それからスマートフォンの Google Authenticator App に秘密キーを共有させます。表示されている QR コードをスキャンするか、「3. QR コードが読めない場合」の下に表示されている 32 文字を手入力することによって共有できます。

その機密キー(上の図で「シード」と書いてあるもの)と時刻を元に、TOTP アルゴリズムで 6 桁の数字のパスワードを計算します。時刻を計算に使うところが Time-based ということだそうです。

さらに 30 秒ごとに計算し直すので、パスワードは 30 秒ごとに違った数字になります。それが One-time ということだそうです。

Google Authenticator App はスタンドアロンで動いています。ASP.NET MVC5 アプリ他どことも通信していませんが秘密キーとスマートフォンの内部時刻でパスワードは計算できます。

秘密キーは共有しているので、ASP.NET MVC5 アプリと Google Authenticator App が取得する時刻が正確に一致していれば、計算で求めたパスワードは両方で同じになります。

という訳で、2 要素認証が必要になっている ASP.NET MVC5 アプリから TOTP ベースのパスワードを求められたら、Google Authenticator App を立ち上げて表示されたパスワードを入力すればログインできるという仕組みになっています。

TOPT ベースの 2 要素認証は Visual Studio のテンプレートで作った MVC5 プロジェクトには実装されておらず、ゼロから作っていくことになります。先人の例がないかググって調べてみましたら、Using Google Authenticator with ASP.NET Identity という記事(以下、参考記事と言います)がありましたので参考にさせていただきました。

以下に実装手順を書きます。参考記事には書いてないが必須な実装は補足・追記しました。また、QR コードがスキャンできない場合の対応など参考記事とは違う実装にしている点もあります。

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

プロジェクトの作成方法は先の記事「MVC5 アプリに 2 要素認証を実装」を見てください。それに Google Authenticator App 利用の 2 要素認証を追加で実装します。

まず、TOTP 用の秘密キーの生成やユーザーが入力したパスワードの検証を行うためのライブラリ OtpSharp を NuGet からインストールします。Base32 という NuGet パッケージも必要ですが一緒にインストールされるはずです。

OtpSharp をインストール

(2) ApplicationUser クラスにプロパティ追加

Models/IdentityModel.cs ファイルの ApplicationUser クラスに以下の 2 つのプロパティを追加します。

public class ApplicationUser : IdentityUser
{
    // Google Authnticator による 2FA を実装するため追加
    public bool IsGoogleAuthenticatorEnabled { get; set; }
    public string GoogleAuthenticatorSecretKey { get; set; }

    // 以下は既存のコード
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
                           UserManager<ApplicationUser> manager)
    {
        var userIdentity = await manager.CreateIdentityAsync(this, 
                    DefaultAuthenticationTypes.ApplicationCookie);
        return userIdentity;
    }
}

Migration 操作によって既存の SQL Server データベースの AspNetUsers テーブルに同名の列が追加されます。アプリを実行して Google Authenticator を有効にすると、IsGoogleAuthenticatorEnabled 列は True になり、GoogleAuthenticatorSecretKey には秘密キーが自動生成され格納されます。

(3) トークンプロバイダの作成

Google Authenticator を使った 2 要素認証用のプロバイダを作成します。(SMS と Email を使った 2 要素認証用のプロバイダは既存です)

IUserTokenProvider<TUser, TKey> インターフェイスを継承した GoogleAuthenticatorTokenProvider クラスを以下のように作成します。クラスファイルは MfaTokenProvider というフォルダを作ってそこに置きました。

using MVC5TwoFactorAuth.Models;
using Microsoft.AspNet.Identity;
using System.Threading.Tasks;
using OtpSharp;
using Base32;

namespace MVC5TwoFactorAuth.MfaTokenProvider
{
    public class GoogleAuthenticatorTokenProvider 
        : IUserTokenProvider<ApplicationUser, string>
    {
        public Task<string> GenerateAsync(string purpose, 
                                          UserManager<ApplicationUser, string> manager, 
                                          ApplicationUser user)
        {
            return Task.FromResult((string)null);
        }

        public Task<bool> IsValidProviderForUserAsync(UserManager<ApplicationUser, string> manager, 
                                                      ApplicationUser user)
        {
            return Task.FromResult(user.IsGoogleAuthenticatorEnabled);
        }

        public Task NotifyAsync(string token, 
                                UserManager<ApplicationUser, string> manager, 
                                ApplicationUser user)
        {
            return Task.FromResult(true);
        }

        public Task<bool> ValidateAsync(string purpose, 
                                        string token, 
                                        UserManager<ApplicationUser, string> manager, 
                                        ApplicationUser user)
        {
            long timeWindowUsed = 0;

            var otp = new Totp(Base32Encoder.Decode(user.GoogleAuthenticatorSecretKey));

            bool valid = otp.VerifyTotp(token, 
                                        out timeWindowUsed, 
                                        new VerificationWindow(previous: 1, future: 1));

            return Task.FromResult(valid);
        }
    }
}

ValidateAsync メソッドで使われている OtpSharp ライブラリの説明は GitHub の記事Otp.NET にあります。

VerifyTotp メソッドの第 1 引数 token はユーザーが Google Authenticator App より取得して入力した 6 桁の数字のパスワード、第 2 引数 timeWindowUsed は検証が行われた回数で、RFC によると 1 回だけにすべきということで、カウントして必要に応じて何かするためのもののようです。

第 3 引数は network delay に関係するもののようですが説明を読んでも分かりませんでした。(汗) 参考記事では new VerificationWindow(2, 2) となっていましたが、RFC 推奨は "one time step delay" ということだそうですので GitHub の記事の通りにしておきました。

GenerateAsync メソッド、NotifyAsync メソッドは今回の実装では使わないということで参考記事の通りにしておきました。(これらのメソッドが呼ばれたら異常事態ということで throw NotImplemantationException() として自爆した方が良かったかもしれません)

(4) GoogleAuthenticatorTokenProvider を登録

上のステップ (3) で作成したプロバイダを登録します。 登録すると、フレームワークがプロバイダに実装されたメソッドを使ってユーザーが入力したパスワードの検証などを行うようです。

App_Start/IdentityConfig.cs ファイルの ApplicationUserManager クラスの Create メソッドに既存のプロバイダ PhoneNumberTokenProvider と EmailTokenProvider を登録するコードがありますので、その下に以下のコードを追加します。

manager.RegisterTwoFactorProvider("GoogleAuthenticator", 
    new GoogleAuthenticatorTokenProvider());

(5) IndexViewModel クラスにプロパティ追加

Models/ManageViewModel.cs ファイルの IndexViewModel クラスに以下のプロパティを追加します。

public class IndexViewModel
{
    public bool HasPassword { get; set; }
    public IList<UserLoginInfo> Logins { get; set; }
    public string PhoneNumber { get; set; }
    public bool TwoFactor { get; set; }
    public bool BrowserRemembered { get; set; }

    // Google Authnticator による 2FA を実装するため追加
    public bool IsGoogleAuthenticatorEnabled { get; set; }
}

さらに、ManageController の Index アクションメソッドに IndexViewModel の IsGoogleAuthenticatorEnabled プロパティを設定するコードを追加します。参考記事には書いてないので注意してください。

var model = new IndexViewModel
{
    HasPassword = HasPassword(),
    PhoneNumber = await UserManager.GetPhoneNumberAsync(userId),
    TwoFactor = await UserManager.GetTwoFactorEnabledAsync(userId),
    Logins = await UserManager.GetLoginsAsync(userId),
    BrowserRemembered = await AuthenticationManager
                              .TwoFactorBrowserRememberedAsync(userId),

    // Google Authnticator による 2FA を実装するため追加
    IsGoogleAuthenticatorEnabled = (await UserManager.FindByIdAsync(userId))
                                   .IsGoogleAuthenticatorEnabled
};

(6) 有効化/無効化を操作するコードを View に追加

Views/Manage/Index.cshtml ファイルの下の方(最後の <dl> 要素の直前)に、Google Authenticator の有効化/無効化を切り替えるための以下のコードを追加します。

<dt>Google Authentication:</dt>
<dd>
    @if (Model.IsGoogleAuthenticatorEnabled)
    {
        using (Html.BeginForm("DisableGoogleAuthenticator", "Manage",
            FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
        {
            @Html.AntiForgeryToken()
            <text>有効
                <input type="submit" value="無効化" class="btn btn-link" />
            </text>
        }
    }
    else
    {
        <text>無効
            @Html.ActionLink("有効化", "EnableGoogleAuthenticator")
        </text>
    }
</dd>

(7) GoogleAuthenticatorViewModel クラスを追加

Models/ManageViewModel.cs ファイルに GoogleAuthenticatorViewModel クラスを追加を追加します。参考記事には書いてないので注意してください。

public class GoogleAuthenticatorViewModel
{
    public string SecretKey { get; set; }
    public string BarcodeUrl { get; set; }

    [Required]
    [Display(Name = "コード")]
    public string Code { get; set; }
}

(8) アクションメソッドの追加

Controllers/ManageController.cs の ManageController に、上のステップ (6) で View に追加した submit ボタンと ActionLink の受信先のアクションメソッド EnableGoogleAuthenticator と DisableGoogleAuthenticator を追加します。

using 句に OtpSharp, Base32, System.Text を追加するのを忘れないようにしてください。

//
// GET: /Manage/DisableGoogleAuthenticator
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> DisableGoogleAuthenticator()
{
    var user = await UserManager
                     .FindByIdAsync(User.Identity.GetUserId());

    if (user != null)
    {
        user.IsGoogleAuthenticatorEnabled = false;
        user.GoogleAuthenticatorSecretKey = null;
        await UserManager.UpdateAsync(user);
        await SignInManager.SignInAsync(user, 
                                        isPersistent: false, 
                                        rememberBrowser: false);
    }

    return RedirectToAction("Index", "Manage");
}

//
// GET: /Manage/EnableGoogleAuthenticator
public ActionResult EnableGoogleAuthenticator()
{
    byte[] secretKey = KeyGeneration.GenerateRandomKey(20);
    string userName = User.Identity.GetUserName();

    string authenticatorUriFormat = 
        "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";

    string authnticatorUri = string.Format(
        authenticatorUriFormat,
        HttpUtility.UrlEncode("MVC5TwoFactorAuth"),  // issuer
        HttpUtility.UrlEncode(userName),
        Base32Encoder.Encode(secretKey));

    // QR コードが読めなかった場合の共有キーの表示
    string keyCode = Base32Encoder.Encode(secretKey);
    ViewBag.KeyCode = FormatKey(keyCode);

    var model = new GoogleAuthenticatorViewModel
    {
        SecretKey = keyCode,
        BarcodeUrl = authnticatorUri
    };

    return View(model);
}

//
// POST: /Manage/EnsableGoogleAuthenticator
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> EnableGoogleAuthenticator(
                                   GoogleAuthenticatorViewModel model)
{
    if (ModelState.IsValid)
    {
        byte[] secretKey = Base32Encoder.Decode(model.SecretKey);

        long timeWindowUsed = 0;
        var otp = new OtpSharp.Totp(secretKey);
        if (otp.VerifyTotp(model.Code,
                           out timeWindowUsed,
                           new VerificationWindow(previous: 1, future: 1)))
        {
            var user = await UserManager
                             .FindByIdAsync(User.Identity.GetUserId());
            user.IsGoogleAuthenticatorEnabled = true;
            user.GoogleAuthenticatorSecretKey = model.SecretKey;
            await UserManager.UpdateAsync(user);

            return RedirectToAction("Index", "Manage");
        }
        else
        {
            ModelState.AddModelError("Code", "コードが有効ではありません");
        }
    }

    ViewBag.KeyCode = FormatKey(model.SecretKey);
    return View(model);
}

// 共有キーを 4 文字ずつ分けて表示するためのヘルパーメソッド
private string FormatKey(string unformattedKey)
{
    var result = new StringBuilder();
    int currentPosition = 0;
    while (currentPosition + 4 < unformattedKey.Length)
    {
        result.Append(unformattedKey.Substring(currentPosition, 4))
              .Append(" ");
        currentPosition += 4;
    }
    if (currentPosition < unformattedKey.Length)
    {
        result.Append(unformattedKey.Substring(currentPosition));
    }

    return result.ToString().ToLowerInvariant();
}

参考記事のコードから変更・追加を行っています。主な点は以下の通りです。

  1. QR コードが読めない場合に認証アプリに秘密キーを直接入力できるよう、この記事の一番上の画像にあるように秘密キーを表示するようにしました。4 文字ずつ分けて表示しているのは見やすくするためです。4 文字ずつ分けるためのヘルパーメソッド FormatKey を追加しています。
  2. QR コードの生成・表示は Mictosoft のドキュメント「ASP.NET Core での TOTP authenticator アプリの QR コード生成を有効にする」に書いてある qrcode.js を使う方法に変更しました。
  3. VerifyTotp メソッドの第 3 引数は、上のステップ (3) で述べたように、RFC 推奨は "one time step delay" ということだそうですので GitHub の記事の通りにしておきました。

(9) View を追加

上のステップ (8) で追加した EnableGoogleAuthenticator アクションメソッドに対応するビュー EnableGoogleAuthenticator.cshtml を追加しします。

@model MVC5TwoFactorAuth.Models.GoogleAuthenticatorViewModel

@{
    ViewBag.Title = "Google Authenticator の利用";
}

<h2>Google Authenticator の有効化</h2>

<div class="row">
    <div class="col-md-8">
        <h3>1. Google Authenticator による 2 要素認証の有効化</h3>
        <p>スマートフォンの Google Authenticator アプリを起動して右の 
        QR コードをスキャンしてください。</p>
        <h3>2. QR コードをスキャンして得られた 6 桁の数字を入力</h3>
        <p>
            QR コードをスキャンすることにより得られた 
            6 桁の数字を下の[コード]欄に入力し、
            [有効化]ボタンをクリックしてください。
        </p>
        <h3>3. QR コードが読めない場合</h3>
        <p>
            共有キー: <kbd>@ViewBag.KeyCode</kbd> を Google Authenticator 
            アプリに入力して得られた 6 桁の数字を下の[コード]欄に入力し、
            [有効化]ボタンをクリックしてください。
        </p>
        <hr />
        @using (Html.BeginForm("EnableGoogleAuthenticator", 
            "Manage", 
            FormMethod.Post, 
            new { @class = "form-horizontal", role = "form" }))
        {
            @Html.AntiForgeryToken()
            @Html.HiddenFor(m => m.SecretKey)
            @Html.HiddenFor(m => m.BarcodeUrl)
            <div class="form-group">
                @Html.LabelFor(m => m.Code, 
                       new { @class = "col-md-2 control-label" })
                <div class="col-md-10">
                    @Html.TextBoxFor(m => m.Code, 
                                     new { @class = "form-control" })
                    @Html.ValidationMessageFor(m => m.Code, "", 
                                     new { @class = "text-danger" })
                </div>
            </div>
            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" class="btn btn-primary" 
                           value="有効化" />
                </div>
            </div>
        }
    </div>
    <div class="col-md-4">
        <div id="qrCode"></div>
        <div id="qrCodeData" data-url="@Html.Raw(Model.BarcodeUrl)"></div>
    </div>
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")

    <script src="~/Scripts/qrcode.js"></script>
    <script src="~/Scripts/qr.js"></script>
}

上のステップ (8) で述べましたように QR コードの生成・表示は qrcode.js を使う方法に変更しました。

(10) Migration の実施

Visual Studio のパッケージマネージャーコンソールで Add-Migration GoogleAuth, Update-Database を実行します。SQL Server データベースの AspNetUsers テーブルに IsGoogleAuthenticatorEnabled 列と GoogleAuthenticatorSecretKey 列が追加されていることを確認してください。


以上で Google Authenticator を使った 2 要素認証機能は動くようになるはずです。簡単に検証手順を書いておくと:

自分のスマートフォンに Google Authenticator App をインストールします。

MVC5 アプリを実行し、画面右上の[登録]をクリックしてユーザー登録を行ってください。登録に成功すると登録したユーザーでログイン済になるはずです。

画面の右上に「ようこそ・・・」と表示されているはずですが、それをクリックすると以下のように Manage/Index 画面に遷移します。

Manage/Index 画面

まず上の画面の「2 要素認証:」の[有効化]をクリックしてそのユーザーの 2 要素認証を有効にします。その操作で AspNetUsers テーブル の TwoFaxtorEnabled 列が True になります。それをしないと、下の「Google Authenticator」で[有効化]操作を行っても 2 要素認証は働かないので注意してください。

「Google Authenticator」の[有効化]をクリックするとこの記事の一番上の画像の Manage/EnableGoogleAuthenticator 画面に遷移しますので、その画面に書いてある通り、スマートフォンの Google Authenticator App を立ち上げて、QR コードをスキャンするか表示されている共有キーを手入力します。

それによりスマートフォンの Google Authenticator App に 6 桁の数字のパスワードが表示されます。 それを[コード]欄に入力して[有効化]ボタンをクリックすれば MVC5 アプリでそのユーザーの Google Authenticator を使っての 2 要素認証が有効になります。

その後、一旦ログオフしてからログインして 2 要素認証が有効になっているか試します。画面右上の[ログイン]をクリックしてログイン画面で[電子メール]と[パスワード]欄に正しく入力して[ログイン]の欄をクリックすると以下の Account/SendCode 画面に遷移します。

Account/SendCode 画面

そこに表示されているドロップダウンリストにはそのユーザーに対して有効になっている 2 要素認証の項目が表示されます。[GoogleAuthentication]を選択して、[送信]ボタンをクリックすると、以下のように Account/VerifyCode 画面に遷移します。(赤枠の部分は自分が便宜上追加したものでオリジナルのコードには含まれませんので注意))

Account/VerifyCode 画面

スマートフォンの Google Authenticator App を開いてそれに表示されている 6 桁の数字のパスワードを上の画面の[コード]欄に入力して[送信]ボタンをクリックするとログインできます。

Tags: , , , ,

MVC

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

by WebSurfer 27. October 2021 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

About this blog

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

Calendar

<<  August 2022  >>
MoTuWeThFrSaSu
25262728293031
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar