🦜

外部テナントで顧客ユーザー管理 ― 基本的なMFA認証を組み込んでみよう(C#)

に公開

はじめに

この記事シリーズでは、外部テナントのEntra External IDを使用した、業務アプリを利用する顧客ユーザーのMFA認証やEntraIDからの情報取得や操作に関する実装について書いていきます。

この記事では、C#(.NET)アプリに顧客ユーザーのMFA認証(IDパスワード+メールOTPの2段階)を組み込むための実装方法について書きます。やりたいことによって設定内容が変わってきますが、まず第一弾として、単純に「ユーザー自身がサインアップ」して「ログイン時にMFA認証を要求」という状態を作っていきます。

構築手順概要

外部テナントで顧客ユーザーのMFA認証を組み込むには、ざっと以下作業が必要になります。

  1. 【EntraID】外部テナントの作成
  2. 【EntraID】アプリの作成・登録
  3. 【EntraID】認証方法(ポリシー)の確認
  4. 【EntraID】条件付きアクセスポリシー設定
  5. 【EntraID】ユーザーフロー作成
  6. 【C#】アプリへの組み込み
  7. 【EntraID】アプリ設定
    • リダイレクトURL
    • APIアクセス許可

EntraIDの操作は、Azureポータルでも可能ですが、基本的にMicrosoft Entra 管理センター(通称:Entraポータル) を使います。

理由としては、

  • MS公式ドキュメントでも、CIAMの操作はEntraポータルを前提としている
  • Entraポータルでしかできない項目がある可能性あり(最新機能の反映もEntraポータルから)
  • CIAM構成の設定利用を前提としているため、Azureポータルより使いやすい

それでは順番にやっていきましょう。Entraポータルへのログインは下記より↓
https://entra.microsoft.com/

1. 【EntraID】外部テナントの作成

まずは、顧客ユーザーを管理するテナントを作成します。
左メニューからEntra IDの「概要」 を選択し、「テナントの管理」。

「作成」を押し、 テナントの構成 で「外部」を選択して「続行」。

作成するテナント情報を入力し、「確認および作成」。

※ちなみに現時点で国に日本はありませんでした。

※"現在プレビュー段階"って書いてありますが、GAされているので課金されるはずです・・・

これで、テナント作成は完了です。
作成したテナントはテナント一覧上に表示されるようになります。
「テナントの種類:外部」と表示され、EntraIDのアイコンもいつもと少し違いますね。

以上で、テナントの作成は完了です。

2. 【EntraID】アプリの作成・登録

作成した外部手テナントに、今回Entra認証を利用するアプリを登録します。EntraIDにアプリを認識させ、認証やAPIアクセスを可能にするために必要な手順です。

左メニューからEntra IDの「アプリの登録」を選択し、「新規登録」。

登録するアプリの情報を入力して「登録」。

入力内容は下記の通り。

[名前] アプリの表示名

アプリ一覧に表示されます。

[アカウントの種類] シングルテナント

今回、アプリを利用するのはこのテナントのユーザーのみなのでシングルテナントを選択。(複数のEntraIDを利用した認証などの場合は、マルチテナントなど)

[リダイレクトURI] 未設定

アプリ作成後に登録するので一旦未設定で進みます。

以上で、アプリ登録は完了です。

「アプリの登録」に表示されるアプリ一覧から、今回登録したアプリの詳細が確認できます。このうち、アプリケーション(クライアント)IDディレクトリ(テナント)IDは後ほど(Azureではなく)アプリ側の設定で必要になります。

3. 【EntraID】認証方法(ポリシー)の確認

外部ユーザーが使用できる認証方法を定義しておくための手順です。
※実際に何で認証するかは条件付きアクセスポリシーやユーザーフローの設定内容で決まります。

左メニューからEntra IDの「認証方法」 を選択し、「ポリシー」を表示します。

今回使用するメールOTPがすべてのユーザーに対して有効になっているか確認します。
※デフォルトで有効になっているはずですが、念のため確認。

4. 【EntraID】条件付きアクセスポリシー設定

Entraユーザーに対しどのような条件で認証を行うかを設定します。MFAによる2段階認証の設定もここから行います。

左メニューからEntra IDの「条件付きアクセス」 を選択し、「ポリシー」→「新しいポリシー」。
※条件付きアクセス画面のコンテンツ側(右側)のローディングが永遠続くことがありますが、気にせずメニューの「ポリシー」を押せば大丈夫です。

ポリシーを設定していきます。

[名前] ポリシーの表示名

一覧上に表示されます。

[ユーザー] 対象:すべてのユーザー、対象外:自分(管理者)

誰に対してポリシーを適用するか。
状況によりけりですが、管理者自身のテスト・デバッグ・構成ミス復旧を確保するため、上記のようにするのが推奨のよう。

[ターゲットリソース] アプリ登録で作成したアプリを選択

どのリソースの認証時にポリシーを適用するか。
これも状況によりけりですが、「すべてのリソース」にすると Entra 管理ポータルや Graph API にも影響し事故の元になるため、必要なリソースのみを指定するのが良いとのこと。

[アクセス制御 許可]
・「アクセス権の付与」を選択
・「多要素認証を要求する」に☑

ターゲットリソースへユーザーがアクセスしたときに、どのような制御をするか。
MFA認証をしたい場合、この「多要素認証を要求する」チェックが必須です。チェックがないと、ただの1段階認証になります。

[ポリシーの有効化] オン

ここをオンにしないと、どんなに設定してもポリシーは適用されません。
逆に言うと、ここをオフにさえすればポリシーは外れます。

すべて設定したら、「作成」を押します。
これで、条件付きアクセスポリシーの設定は完了です。

作成したポリシーは一覧上に表示され、あとから編集も可能です。

5. 【EntraID】ユーザーフローの作成

外部ユーザーのサインアップ・サインインの挙動を定義します。

左メニューからEntra IDの「External Identities」を選択し、「ユーザーフロー」→「新しいユーザーフロー」。

作成するユーザーフローの情報を入力します。

[名前] ユーザーフローの表示名

ユーザーフロー一覧に表示されます。

[IDプロバイダー] パスワードを含むメール

1段階目の認証方法になります。
日本語が微妙ですが、「パスワードを含むメール」はいわゆる一般的なID(メールアドレス)とパスワードによる認証のことです。

現時点では、2段階目の認証が基本的にメールOTPのため、MFA認証前提だと「パスワードを含むメール」しか選べません
※ちなみに、ここでメールOTPを選択すると、MFA認証時に、1段階目の認証(メールOTP)が完了したタイミングでエラーになります。

MFA認証をしない場合、どちらも選択可能で、メールOTPを選択した場合、ユーザーはパスワード設定する必要はなく、毎回OTPのみでのログインになります。(わかる人にしかわからない話ですが、Minecraftのサインインがまさにそれです。)

[ユーザー属性] 未設定

サインアップ時に登録する(ユーザーに入力させる)項目です。
Entra上の項目(すべてではない)だけでなく、任意の属性(カスタム属性)もサインアップ時に登録できるようになります。
別の回で詳しく説明するとして、今回はスキップします。

設定したら、「作成」を押下します。
次に、作成したユーザーフローがアプリから呼び出されるよう紐づけます。

ユーザーフローの一覧に戻り、作成したユーザーフローを選択します。
「アプリケーション」→「アプリケーションの追加」から、手順2で登録したアプリを選択して追加します。

以上で、ユーザーフローの作成は完了です。

6. 【C#】アプリへの認証組み込み

上記で設定した認証をアプリに組み込んでいきます。

今回はC#(.NET9、ASP.NET Core MVC)で簡単なサンプルプログラムを作ったので、これに沿って説明していきます。
https://github.com/YuFuru513/entra-external-id-samples/tree/main/01-basic-mfa-csharp

  • サンプルのプロジェクト構成
BasicMfaSample.sln
├── BasicMfaSample.Models      # データモデル層
├── BasicMfaSample.Services    # ビジネスロジック・認証サービス層
└── BasicMfaSample.Web         # MVC プレゼンテーション層
  • サンプルの挙動
    ホーム(認証不要画面)⇒ログイン(Entraの認証連携部分)⇒ダッシュボード(要認証画面)
    これだけです。ログイン部分に関しては、上記の手順に従っていればMFA認証が発動します。
    以下、実装上ポイントとなる点を抜粋して説明します。

追加パッケージ

実装にあたり、下記のNuGetパッケージが必要になります。

  • Microsoft.AspNetCore.Authentication.OpenIdConnect
    ASP.NET Core の認証ミドルウェアです。Program.cs での認証設定に必要になります。
    サンプルでは、BasicMfaSample.Webにインストールされています。

  • Microsoft.Identity.Web
    Entra ID (Azure AD) との連携ロジックです。サービス層での認証処理に必要になります。
    サンプルでは、BasicMfaSample.Servicesにインストールされています。

appsetting.json

Program.csから参照されたり、内部的に参照されるAzureの環境情報を記載しています。
ここで「2.検証アプリの作成・登録」手順で登録したアプリの情報(テナント名、テナントID、アプリのクライアントID)が必要になります。
https://github.com/YuFuru513/entra-external-id-samples/blob/main/01-basic-mfa-csharp/BasicMfaSample.Web/appsettings.json

/BasicMfaSample.Web/appsettings.json
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath": "/signout-callback-oidc",

サンプル上の/signin-oidc/signout-callback-oidcという設定値は、Microsoft.Identity.Web内の各プロパティで内部的に設定されているデフォルト値のため、省略も可能(省略してもこのエンドポイントが使われる)ですが、別のエンドポイントに書き換えたい場合、ここの設定およびEntraID上の対応項目を一致させる必要があります。
(詳しくは[7.【EntraID】アプリ設定 プラットフォーム構成]で説明します)

Program.cs

認証設定の核心部分になります。

- AddAuthenticationでOIDC認証を指定
- AddMicrosoftIdentityWebAppに認証エンドポイントを指定

/BasicMfaSample.Web/Program.cs
//認証サービスの構成
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(options =>
    {
        builder.Configuration.Bind("AzureAd", options);
        //CIAMにおけるAuthorityの書き方(フロー名を入れたり、"p=~"などパラメータ指定する方法はB2Cの認証方法なので×)
        options.Authority = $"{builder.Configuration["AzureAd:Instance"]}{builder.Configuration["AzureAd:TenantId"]}/v2.0";
    });

認証エンドポイントは下記の形になります。
(厳密には、これがベースURLとなり認証やトークン発行等必要なエンドポイントが内部で生成されていきます。)

https://<自身のテナント名>.ciamlogin.com/<自身のテナントID>/v2.0

認可ポリシーの設定方法はいろいろありますが、サンプルでは下記のようにしています。
- .SetFallbackPolicy:デフォルトの認可ポリシー設定
- .RequireAuthenticatedUser:認証済みユーザーのみアクセス可能
全てのControllerで自動的に認証が必要

この上で、認証不要なControllerには明示的に[AllowAnonymous]を付けています。

/BasicMfaSample.Web/Program.cs
//認可ポリシーの設定
// (FallbackPolicyで全ページ認証必要にしておく→不要なページは [AllowAnonymous] で除外)
builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
/BasicMfaSample.Web/Controllers/HomeController.cs
[AllowAnonymous]
public IActionResult Index()
{
    return View();
}

認証ミドルウェアの設定に関しては、認証⇒認可という順序が大事になります。

/BasicMfaSample.Web/Program.cs
//Authentication(認証:あなた誰?)→Authorization(認可:この人にアクセス権ある?)という順序が大事
//UseAuthentication():ユーザー属性データをHttpContext.User(ClaimsPrincipal)に格納
//UseAuthorization():HttpContext.Userを見て認可判断
app.UseAuthentication();
app.UseAuthorization();

EntraAuthService.cs

サインイン時、サインアウト時の認証プロパティを作成します。

RedirectUriは認証処理完了後にユーザーを戻す場所になります。
デフォルト値として、サインイン時は"/Dashboard/Index"、サインアウト時は"/"を指定しているので、サービス呼び出し時に明示的な指定がなければサインイン後はダッシュボードへ、サインアウト時はトップページに戻ります。

/BasicMfaSample.Services/Auth/EntraAuthService.cs
public AuthenticationProperties CreateSignInProperties(string returnUrl = "/Dashboard/Index")
{
    return new AuthenticationProperties { RedirectUri = returnUrl };
}

public AuthenticationProperties CreateSignOutProperties(string returnUrl = "/")
{
    return new AuthenticationProperties { RedirectUri = returnUrl };
}

AccountController.cs

認証関連の操作を担当するコントローラです。

サインインボタン押下時に、SignIn()が起動します。
サービス層で作成した認証プロパティを使用し、OpenID Connectの認証フローを呼び出しています。これにより、まずEntra External IDにリダイレクトされ、認証完了後はreturnUrl にリダイレクトされます。

ログアウトボタン押下時に、SignOut()が起動します。
サービス層で作成したログアウト用の認証プロパティを使用し、複数の認証スキーム(EntraID&ローカルCookie)からサインアウトしています。

/BasicMfaSample.Web/Controllers/AccountController.cs

public IActionResult SignIn(string returnUrl = "/Dashboard/Index")
{
    var properties = _authService.CreateSignInProperties(returnUrl);
    return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme);
}

public IActionResult SignOut(string returnUrl = "/")
{
    var properties = _authService.CreateSignOutProperties(returnUrl);
    return SignOut(properties, OpenIdConnectDefaults.AuthenticationScheme, "Cookies");
}

7.【EntraID】アプリ設定

組み込んだアプリの情報をもとに、EntraIDのアプリを設定していきます。

プラットフォーム構成

左メニューからEntra IDの「アプリ登録」 を選択 > 「認証」> 「プラットフォームを追加」。

「Web」を選択し、下記を設定します。

[リダイレクトURI] http://<アプリの実行環境>/signin-oidc
[暗黙的な許可およびハイブリッドフロー] IDトークン

このリダイレクトURIというのは、認証後に到達する画面ではなく認証完了後にEntra External IDからアプリに戻ってくるエンドポイント、もっと厳密にいうと、OpenIDConnectプロトコルの中継地点、OpenIDConnectのAuthorization Code Flowの内部処理で使用されるエンドポイントになります。

サインイン時の流れは下記の通り。

  1. ユーザー: サインインボタンクリック
  2. アプリ: Entra External ID にリダイレクト
  3. ユーザー: MFA認証完了
  4. Entra ID: コールバックエンドポイントに戻る ← ここ
  5. アプリ: 認証コードを受け取りトークンを交換、Cookieにセッション情報を保存(自動処理)
  6. アプリ: 最終目的地にリダイレクト

/signin-oidcというのは、Microsoft.Identity.Web内で内部的に設定されているデフォルトのエンドポイントです。特にこだわりがなければ、このデフォルトのエンドポイントを使用して問題ありません。

一方、設定項目にはしていませんが、[フロントチャネルログアウトURL]というのが、サインアウト時にEntra External IDからアプリに戻ってくるエンドポイント、になります。こちらのデフォルトは/signout-callback-oidcです。

この[リダイレクトURI]および[フロントチャネルログアウトURL]のエンドポイントは、前述のとおりappsetting.jsonの設定値CallbackPathSignedOutCallbackPathと完全に一致させる必要があります。デフォルトのエンドポイント以外を指定する場合は、アプリとEntraIDの両者設定することを忘れずに。

※<アプリの実行環境>は、ローカル実行であればlaunchSettings.jsonのhttpsのapplicationUrlを拾います。

/BasicMfaSample.Web/Properties/launchSettings.json
"applicationUrl": "https://localhost:7059;http://localhost:5224",
                 ↑これ

APIアクセス許可

アプリが保護されたEntraの機能(GraphAPIなど)にアクセスするために必要な権限(スコープ)を定義します。

今回のケースではデフォルト以上に必要なスコープはありませんが、管理者同意の付与をしておきます。管理者同意というのは、アプリケーションの権限要求に対して一般ユーザーが個別に同意するのではなく、管理者が事前に同意を与え、権限を一元管理するための設定です。
外部テナントにおけるEntra External ID (CIAM環境)では、基本的にユーザー個別同意は無効なため、管理者同意が必須となります。

左メニューからEntra IDの「アプリ登録」 を選択 > 「認証」> 「APIのアクセス許可」。
「<テナント名>に管理者の同意を与えます」を押下し、「はい」。

同意が付与されると、付与されたスコープに下記マークがつきます。

設定は以上です。

動作確認

動かしてみます。

サインアップからのサインイン

トップページ。右上の[ログイン]を押します。

EntraIDのログイン画面が表示されました。
今回はユーザーのサインアップからやりたいので、[アカウントをお持ちでない場合~]を押します。

アカウント作成画面が表示されました。
登録したいアカウントのメールアドレスを入力して[次へ]を押します。

入力したアドレス宛に、ワンタイムパスワードが送られてきました。
ワンタイムパスワードを入力して[次へ]を押します。

パスワード設定画面が表示されました。
パスワードおよび再入力して[次へ]を押します。

アカウントを作成してくれているようです。待ちましょう。

アカウントが作成されると、そのままサインイン処理に進みます。
(画面上もサインイン中の画面になるのですがキャプチャ撮れず)
サインインの状態を維持に関して聞かれる画面が表示されました。任意の回答をします。

無事にサインアップ~サインインでき、要認証画面へ到達できました!

EntraID上のユーザー

作成したユーザーをEntraポータルで確認して見ます。

この赤枠の"unknown"さんが今回作成したユーザーです。ID列に設定したメールアドレス、ユーザーの種類はローカルアカウントになっています。
表示名がunknownとなっているのは、EntraID上「表示名」プロパティが未設定のためです。
※ちなみにこの「表示名」プロパティは「氏」「名」とも特に関連しないプロパティのため、仮に「氏」「名」を設定していてもここはunknownのままです。運用上は何らかのタイミングで「表示名」を設定する方が良さそうです。

ログアウト

このまま、ログアウトもしてみます。右上の[ログアウト]を押下します。

サインアウトする対象のアカウントを聞かれました。(ここでは1件ですが)選択します。

未認証の画面に戻ってきました。

通常サインイン

アカウントは作成されているので、通常のログインをしてみます。
EntraIDのログイン画面で、アカウント登録したメールアドレスを入力して[次へ]を押します。

アカウント作成時に登録したパスワードを入力して[サインイン]を押します。

メールOTPの画面が表示されました。「XXXについての電子メールコード」を選択します。

登録しているメールアドレスにワンタイムパスワードが送られてきます。
ワンタイムパスワードを入力して[検証]を押します。

先ほど同様、サインインの状態を維持に関して聞かれる画面が表示されたりします。(省略)
無事にMFA認証を経てサインインできて要認証画面へ到達できました!

その他

明示的に実装しているわけではないですが、デフォルトで下記の機能も備わっています。

  • 各種エラーチェック機能


  • パスワードリセット機能(OTP検証後、パスワードリセット可能)

  • ワンタイムパスワードの再送信機能(一定時間経過後、再送信可能)

おわりに

今回は、実際にAzure上に外部テナントを構築し、Entra環境を整備することでアプリケーションに外部ユーザー自身のサインアップ機能とMFAログインを組み込む手順を説明しました。次の記事では、サインアップ時にユーザーの属性を設定する手順について説明します。

外部テナントで顧客ユーザー管理 シリーズ記事一覧

  1. Azure Entra External ID(知識編)
  2. 基本的なMFA認証を組み込んでみよう(C#)※この記事
  3. サインアップ時に属性を登録させよう(C#)
  4. トークンに属性を追加してみよう(C#)
  5. 基本的なMFA認証を組み込んでみよう(Next.js編)
    【第2章】ユーザー情報操作編
  6. GraphAPIで顧客ユーザー情報を操作しよう(本人編(C#))
  7. GraphAPIで顧客ユーザー情報を操作しよう(他人編(C#))

Discussion