Entra External ID + Blazor App + Web API でB2C認証を実装する
概要
本記事では、Microsoft Entra External ID を活用して、B2C(Business to Consumer)向けのアプリケーションを構築します。
ユーザーは、Blazor アプリケーション上でセルフサインアップ(自分でアカウント登録)を行うことができます。
さらに、サインアッププロセスをカスタマイズすることで、ユーザーが登録時に追加情報(利用者ID、利用規約への同意)などの追加情報を入力できるようにします。これにより、アプリケーション側でユーザー属性に応じた処理や表示を柔軟に行うことが可能になります。
開発環境
主な開発環境は以下の通りです。
- .NET 9.0
- Microsoft Visual Studio 2022 Community (64ビット) Version 17.14.7
- C# 13.0
- Windows 11 Pro 64bit 24H2
プロジェクトの構成
このプロジェクトでは、Microsoft Entra External ID を使って B2C 向けのアプリケーションを構築します。
ユーザーは Blazor アプリ上でセルフサインアップを行い、ログイン後に Web API を通じてサービスを利用できるようになります。
Blazor の公式サンプルをベースに、以下の4つのプロジェクトを作成します。
公式サンプルは、認証情報の発行元URLや設定がEntra External IDの場合とは異なる部分があるため注意が必要です。
また、ユーザーのサインアップ時に追加情報を取得・検証するための API も用意します。
- BlazorWebAppEntra
- Blazorアプリのサーバー側
- BlazorwebAppEntra.Client
- Blazorアプリのクライアント側
- MinimalApiJwt
- Web API
- Blazorアプリから呼び出されるバックエンド
- ValidateCustomer
- サインアップ時に呼び出されるカスタム認証拡張API
- ユーザーが入力した追加情報を検証する
デモアプリをGitHubに公開しています。
前提条件
本記事で紹介する手順を実行するためには有効なAzureサブスクリプションが必要です。
また、事前にEntra External IDのテナントを作成しておいてください。
公式サンプルをダウンロードしてください。
Visual Studio 2022
を使用して開発とAzureへのデプロイを行います。
カスタムユーザー属性の追加
ユーザーから収集する追加情報として、利用者IDと利用規約への同意を格納するカスタム属性を作成します。
- Azure ポータルで、Microsoft Entra External ID のテナントに移動します。
- 「External Identities」>「カスタムのユーザー属性」>「追加」をクリックします。
- 利用者IDを格納する属性を作成します。
- 名前:
CustomerID
- データ型:
文字列
- 説明:
利用者ID
- 名前:
- 「保存」をクリックします。
- 同様に、利用規約への同意を格納する属性を作成します。
- 名前:
TermsOfService
- データ型:
Boolean
- 説明:
利用規約への同意
- 名前:
- 「保存」をクリックします。
Microsoft Entra External ID アプリの登録
Microsoft Entra External ID のテナントに各アプリを登録します。
バックエンド API の登録(MinimalApiJwt)
バックエンド API を登録します。
- Azure ポータルで、Microsoft Entra External ID のテナントに移動します。
- 「アプリの登録」>「新規登録」をクリックします。
- 名前に
MinimalApiJwt
と入力します。 - 「サポートされているアカウントの種類」は「この組織のディレクトリ内のアカウントのみ」を選択します。
- 「リダイレクト URI」は空欄のままにして、「登録」をクリックします。
- 登録後、APIを公開します。「API の公開」>「Scope の追加」をクリックします。
- 「アプリケーション ID の URI」は
api://<Application (client) ID>
の形式で自動的に設定されます。「保存してから続ける」をクリックします。 - 「スコープ名」には
Weater.Get
と入力します。 - 「同意できるのはだれですか?」に「管理者とユーザー」を選択します。
- 「管理者の同意の表示名」には
気象データの取得
、「管理者の同意の説明」には気象データを取得するためのアクセス許可
と入力します。 - 「ユーザーの同意の表示名」と「ユーザーの同意の説明」は管理者と同じ内容を入力しても、空欄でも構いません。
- 「状態」は「有効」のままにして、「スコープの追加」をクリックします。
Blazor アプリの登録(BlazorWebAppEntra)
フロントエンドの Blazor アプリを登録します。
- Azure ポータルで、Microsoft Entra External ID のテナントに移動します。
- 「アプリの登録」>「新規登録」をクリックします。
- 名前に
BlazorWebAppEntra
と入力します。 - 「サポートされているアカウントの種類」は「この組織のディレクトリ内のアカウントのみ」を選択します。
- 「リダイレクト URI」はプラットフォームに
Web
を選択して、URIにhttps://localhost/signin-oidc
と入力します。 - 「登録」をクリックします。
- 登録後、「認証」ページにある「フロントチャネルのログアウト URL」に
https://localhost/signout-callback-oidc
と入力します。 - 「保存」をクリックします。
- APIへのアクセス許可を追加します。「API のアクセス許可」>「アクセス許可の追加」をクリックします。
- 「所属する組織で使用している API」タブを選択し、
MinimalApiJwt
を選択します。 - 「アプリケーションに必要なアクセス許可の種類」では「委任されたアクセス許可」を選択します。アプリケーションにサインインしたユーザーとして API にアクセスするためです。
- 「アクセス許可」には、先ほど作成した
Weater.Get
を選択します。 - 「アクセス許可の追加」をクリックします。
- 「API のアクセス許可」ページに戻り、「{テナント名} に管理者の同意を付与」ボタンをクリックして、アクセス許可を承認します。管理者の同意を付与すると、ユーザーに対して同意を求めるプロンプトが表示されなくなります。
- 「証明書とシークレット」ページに移動し、「新しいクライアント シークレット」をクリックします。デモのためにクライアントシークレットを使用します。運用環境では、証明書を使用してください。
- 「追加」をクリックします。クライアントシークレットが作成されるので、値をコピーして安全な場所に保存します。後で Blazor アプリの構成に使用します。
- トークンにカスタム属性を含めるようにします。「概要」ページに移動し、「ローカル ディレクトリでのマネージド アプリケーション」にあるリンクをクリックします。
- 「管理」>「シングルサインオン」をクリックします。
- 「属性とクレーム」にある「編集」をクリックします。
- 「新しいクレームの追加」をクリックします。
- 「名前」には
CustomerID
と入力します。 - 「ソース」に「ディレクトリ スキーマ拡張」を選択し、
b2c-extensions-app. Do not modify. Used by AADB2C for storing user data.
アプリを選択して、「選択」をクリックします。 - 「user.CustomerID」を選択し、「追加」をクリックします。
- 「保存」をクリックして、トークンにカスタム属性を追加します。
- 同様に、利用規約への同意を格納する
TermsOfService
属性も追加します。 - アプリケーション マニフェストを更新して、アプリケーションがカスタムクレームの受け取りを許可します。Entra External ID テナントのEntra管理センターへ移動します。
- 「アプリの登録」>
BlazorWebAppEntra
を選択し、「管理」>「マニフェスト」をクリックします。 - 「Microsoft Graph アプリ マニフェスト (新規)」の中から
acceptMappedClaims
キーを見つけ、値をtrue
に変更します。 - 「保存」をクリックします。
ユーザーフローの作成
カスタムユーザー属性を収集する為に、サインアップフローをカスタマイズします。
- Azure ポータルで、Microsoft Entra External ID のテナントに移動します。
- 「External Identities」>「ユーザーフロー」>「新しいユーザーフロー」をクリックします。
- 「名前」に
CustomerSignUp
と入力します。 - 「ID プロバイダー」では「パスワードを含むメール」を選択します。
- 「ユーザー属性」では、以下の属性を選択します。
- Display Name
- CustomerID
- TermsOfService
- 「作成」をクリックします。
- 作成したユーザーフローを開き、「ページ レイアウト」からサインアップページのレイアウトをカスタマイズします。
- CustomerID
- ラベル:
利用者ID
- 必須:
Yes
- ラベル:
- DisplayName
- 必須:
Yes
- 必須:
- TermsOfService
- ラベル:
利用規約への同意
- 必須:
Yes
- ラベル:
- CustomerID
- 「保存」をクリックします。
- 「アプリケーション」 ページに移動し、「アプリケーションの追加」をクリックします。
-
BlazorWebAppEntra
アプリを選択し、「選択」をクリックします。これで、Blazor アプリがこのユーザーフローを使用できるようになります。
条件付きアクセスを構成する
ユーザーのサインインにパスワードとワンタイムパスワードの両方を要求する条件付きアクセス ポリシーを作成します。
- Azure ポータルで、Microsoft Entra External ID のテナントに移動します。
- 「条件付きアクセス」>「ポリシー」>「新しいポリシー」をクリックします。
- 「名前」に
Require MFA for all users
と入力します。 - 「割り当て」>「ユーザー」セクションで、「すべてのユーザー」を選択します。
- 「割り当て」>「ターゲット リソース」セクションで、「リソースの選択」を選択し、
BlazorWebAppEntra
アプリを選択します。 - 「アクセス制御」>「許可」セクションで、「アクセスの付与」を選択し、「多要素認証を要求する」にチェックを入れます。「選択」をクリックします。
- 「ポリシーの有効化」セクションで、「オン」を選択します。
- 「作成」をクリックします。
サーバー プロジェクトを構成する(BlazorWebAppEntra)
-
BlazorWebAppEntraプロジェクトの
appsettings.Development.json
を編集して、Entra External ID テナントに登録したBlazorWebAppEntra
アプリの情報を構成します。{ "AzureAd": { "CallbackPath": "/signin-oidc", "ClientId": "{CLIENT ID (BLAZOR APP)}", "Domain": "{DIRECTORY NAME}.onmicrosoft.com", "Instance": "{認証エンドポイントのベースURL}", "ResponseType": "code", "TenantId": "{TENANT ID}" }, "DownstreamApi": { "BaseUrl": "{BASE ADDRESS}", "Scopes": [ "{APP ID URI}/Weather.Get" ] } }
-
{CLIENT ID (BLAZOR APP)}
: BlazorWebAppEntraアプリの「アプリケーション (クライアント) ID」 -
{DIRECTORY NAME}
: Entra External ID テナントのディレクトリ名 -
{認証エンドポイントのベースURL}
: Entra External IDではhttps://{DIRECTORY NAME}.ciamlogin.com/
の形式で指定します -
{TENANT ID}
: Entra External ID テナントの「ディレクトリ (テナント) ID」 -
{BASE ADDRESS}
: MinimalApiJwtアプリのURL(https://localhost:7277
) -
{APP ID URI}
: MinimalApiJwtアプリの「アプリケーション ID URI」(api://<Application (client) ID>
の形式)
DownstreamApi:Scopes
に{APP ID URI}/.default
を指定した場合は、アプリケーションに付与されたすべてのスコープが含まれます。 -
-
ユーザーシークレットに、BlazorWebAppEntraアプリのクライアントシークレットを追加します。
dotnet user-secrets set "AzureAd:ClientSecret" "{CLIENT SECRET}"
-
Program.cs
を編集して、認証とAPI呼び出しの設定を追加します。builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"), subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true) .EnableTokenAcquisitionToCallDownstreamApi() .AddDownstreamApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi")) .AddDistributedTokenCaches();
AddMicrosoftIdentityWebApp
メソッドのsubscribeToOpenIdConnectMiddlewareDiagnosticsEvents
パラメーターをtrue
に設定すると、認証プロセスの詳細なログが取得できるため、トラブルシューティングに役立ちます。
バックエンド Web API プロジェクトを構成する(MinimalApiJwt)
-
MinimalApiJwtプロジェクトの
appsettings.Development.json
を編集して、Entra External ID テナントが発行したJWTトークンを検証するための情報を構成します。"Authentication": { "Schemes": { "Bearer": { "Authority": "https://{DIRECTORY NAME}.ciamlogin.com/{TENANT ID (WEB API)}/v2.0", "ValidAudiences": [ "{CLIENT ID (WEB API)}" ] } } }
-
{DIRECTORY NAME}
: Entra External ID テナントのディレクトリ名 -
{TENANT ID (WEB API)}
: MinimalApiJwtアプリの「ディレクトリ (テナント) ID」 -
{CLIENT ID (WEB API)}
: MinimalApiJwtアプリの「アプリケーション (クライアント) ID」 -
※ JWTトークンの発行者(
Authority
)は、https://{DIRECTORY NAME}.ciamlogin.com/{TENANT ID}/v2.0
の形式で指定します。 -
※ ValidAudiencesは
api://<Application (client) ID>
の形式ではないので注意
-
-
Program.cs
を編集して、JWTベアラートークンの認証を追加します。builder.Services.AddAuthentication() .AddJwtBearer("Bearer", jwtOptions => { var config = builder.Configuration.GetSection("Authentication:Schemes:Bearer"); jwtOptions.Authority = config["Authority"]; jwtOptions.TokenValidationParameters.ValidAudiences = config.GetSection("ValidAudiences").Get<string[]>(); }); builder.Services.AddAuthorization();
サインアップ時のカスタム認証拡張 API プロジェクトを作成する(ValidateCustomer)
-
新しい ASP.NET Core Web API プロジェクトを作成します。プロジェクト名は
ValidateCustomer
とします。 -
Entra External ID のサインアップ フローにおいて、ユーザーが入力したカスタム属性を検証する為に、
OnAttributeCollectionSubmit
(属性コレクションの送信)イベントを処理するエンドポイントを実装します。 -
OnAttributeCollectionSubmit
イベントの要求ペイロードを格納する為に、AttributeSubmitRequest
クラスを作成します。Models/AttributeSubmitRequest.csusing System.Text.Json.Serialization; namespace ValidateCustomer.Models { public class AttributeSubmitRequest { public string Type { get; set; } = default!; public Data Data { get; set; } = default!; } public class Data { public UserSignUpInfo UserSignUpInfo { get; set; } = default!; } public class UserSignUpInfo { public Dictionary<string, AttributeValue> Attributes { get; set; } = default!; public List<Identity> Identities { get; set; } = default!; } public class AttributeValue { [JsonPropertyName("@odata.type")] public string Type { get; set; } = default!; public object Value { get; set; } = default!; public string AttributeType { get; set; } = default!; } public class Identity { public string SignInType { get; set; } = default!; public string Issuer { get; set; } = default!; public string IssuerAssignedId { get; set; } = default!; } }
-
カスタム属性の検証がOKの場合に継続応答を返す
ContinueWithDefaultResponseObject
クラスと、検証エラーとメッセージの表示を指示するShowValidationErrorResponseObject
クラスを作成します。また、各応答の基底クラスとなるAttributeCollectionSubmitResponseDataBase
クラスも作成します。Models/AttributeCollectionSubmitResponseDataBase.csusing System.Text.Json.Serialization; namespace ValidateCustomer.Models { public abstract class AttributeCollectionSubmitResponseDataBase { [JsonPropertyName("@odata.type")] public string Type { get => "microsoft.graph.onAttributeCollectionSubmitResponseData"; } } }
Models/ContinueWithDefaultResponseObject.csusing System.Text.Json.Serialization; namespace ValidateCustomer.Models { public class ContinueWithDefaultResponseObject { [JsonPropertyName("data")] public ContinueWithDefaultData Data { get; set; } = default!; } public class ContinueWithDefaultData : AttributeCollectionSubmitResponseDataBase { [JsonPropertyName("actions")] public List<ContinueWithDefaultBehavior> Actions { get; set; } = default!; } public class ContinueWithDefaultBehavior { [JsonPropertyName("@odata.type")] public string Type { get => "microsoft.graph.attributeCollectionSubmit.continueWithDefaultBehavior"; } } }
Models/ShowValidationErrorResponseObject.csusing System.Text.Json.Serialization; namespace ValidateCustomer.Models { public class ShowValidationErrorResponseObject { [JsonPropertyName("data")] public ShowValidationErrorData Data { get; set; } = default!; } public class ShowValidationErrorData : AttributeCollectionSubmitResponseDataBase { [JsonPropertyName("actions")] public List<ShowValidationError> Actions { get; set; } = default!; } public class ShowValidationError { [JsonPropertyName("@odata.type")] public string Type { get => "microsoft.graph.attributeCollectionSubmit.showValidationError"; } [JsonPropertyName("message")] public string Message { get; set; } = default!; } }
-
属性コレクションの送信イベントを処理する
AttributeSubmitController
コントローラーを作成します。Controllers/AttributeSubmitController.csusing ValidateCustomer.Models; namespace ValidateCustomer.Controllers { [ApiController] [Route("api/[controller]")] public class AttributeSubmitController : ControllerBase { private readonly ILogger<AttributeSubmitController> _logger; private readonly string_clientId; private readonly List<string> _validCustomerId = new List<string> { "1", "2", "3", "4", "5", }; public AttributeSubmitController(IConfiguration configuration, ILogger<AttributeSubmitController> logger) { _logger = logger; _clientId = configuration["ClientId"] ?? throw new ArgumentNullException("ClientId"); } [HttpPost] public IActionResult Post(AttributeSubmitRequest request) { var customerIdKey = $"extension_{_clientId}_CustomerID"; var customerId = request.Data.UserSignUpInfo.Attributes.ContainsKey(customerIdKey) ? request.Data.UserSignUpInfo.Attributes[customerIdKey].Value.ToString() : null; if (string.IsNullOrWhiteSpace(customerId)) { // 利用者IDが未入力の場合、バリデーションエラーを返す var response = new ShowValidationErrorResponseObject { Data = new ShowValidationErrorData { Actions = new List<ShowValidationError>{ new ShowValidationError { Message = "利用者IDを入力してください。"} } } }; return Ok(response); } if (_validCustomerId.Contains(customerId)) { // 利用者IDが正しい場合、継続応答を返す var response = new ContinueWithDefaultResponseObject { Data = new ContinueWithDefaultData { Actions = new List<ContinueWithDefaultBehavior>{ new ContinueWithDefaultBehavior() } } }; return Ok(response); } else { // 利用者IDが誤っている場合、バリデーションエラーを返す var response = new ShowValidationErrorResponseObject { Data = new ShowValidationErrorData { Actions = new List<ShowValidationError>{ new ShowValidationError { Message = "利用者IDが正しくありません。"} } } }; return Ok(response); } } } }
-
Program.cs
を編集して、コントローラーの設定を追加します。Program.csvar builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); builder.Services.AddControllers(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.UseHttpsRedirection(); app.MapControllers(); app.Run();
-
ユーザーカスタム属性の名前は
extension_{appIdWithoutHyphens}_{customAttributeName}
の形式になります。Entra External ID テナントに登録されているb2c-extensions-app. Do not modify. Used by AADB2C for storing user data.
アプリのアプリケーション (クライアント) ID を確認します。-
{appIdWithoutHyphens}
は、ユーザーのカスタム属性を保存するアプリのアプリケーション (クライアント) ID のハイフンを除いた文字列です。 -
{customAttributeName}
は、カスタム属性の名前です。
-
カスタム認証拡張機能 API を App Service にデプロイする
Entra External ID のサインアップフローから呼び出せるように、ValidateCustomerプロジェクトをAzure App Serviceにデプロイします。
- Azure ポータルで、Microsoft Entra ID のテナントに移動します。
- 「App Services」>「作成」>「Web アプリ」をクリックします。
- 必要な情報を入力します。
- サブスクリプション: 有効なサブスクリプションを選択します。
- リソース グループ: 新しいリソース グループを作成するか、既存のリソース グループを選択します。
- 名前:
validate-customer-api
- 公開: 「コード」を選択します。
- ランタイム スタック:
.NET 9 (STS)
を選択します。 - オペレーティング システム:
Windows
を選択します。 - リージョン: 適切なリージョンを選択します。
- Windows プラン: 新しい
Free F1
プランを作成するか、既存のFree F1
プランを選択します。
- 「確認および作成」をクリックし、設定を確認してから「作成」をクリックします。
-
validate-customer-api
の「設定」>「環境変数」>「追加」をクリックして、カスタム属性の検証に必要なClientId
を追加します。- 名前:
ClientId
- 値:
{APP ID WITHOUT HYPHENS}
- スロット設定:
オフ
- 名前:
- 「適用」をクリックして環境変数を保存します。
- 「概要」ページに移動し、「再起動」をクリックして App Service を再起動し、環境変数を反映させます。
- Visual Studioに戻り、
ValidateCustomer
プロジェクトを右クリックし、「発行」を選択します。 - 「ターゲットの選択」画面で「Azure」を選択し、「次へ」をクリックします。
- 「Azure App Service (Windows)」を選択し、「次へ」をクリックします。
- 「サブスクリプション」ドロップダウンから有効なサブスクリプションを選択します。
- 先ほど作成した
validate-customer-api
を選択し、「次へ」をクリックします。 - API Management は使用しないので、「この手順をスキップする」を選択してから「次へ」をクリックします。
- 「完了」をクリックして発行プロファイルを作成します。
- 作成した発行プロファイルの「発行」ボタンをクリックして、API を Azure App Service にデプロイします。
サインアップ時のカスタム認証拡張 API の構成
Entra External ID のユーザーフローのおいて、属性コレクションの送信イベント時に validate-customer-api
を呼び出すように構成します。
- Azure ポータルで、Microsoft Entra External ID のテナントに移動します。
- 「External Identities」>「カスタム認証拡張機能」>「カスタム拡張機能の作成」をクリックします。
- 「基本」セクションでは、「AttributeCollectionSubmit」を選択し、「次へ」をクリックします。
- 「エンドポイントの構成」セクションは以下の通りに入力し、「次へ」をクリックします。
- 名前:
ValidateCustomerAPI
- 対象 URL:
https://{validate-customer-apiの規定のドメイン}/api/attributesubmit
- 説明:
サインアップ時のカスタム属性検証
- 名前:
- 「アプリ登録の種類」セクションは以下の通りに入力し、「次へ」をクリックします。
- アプリ登録の種類:
アプリの登録を新規作成する
- 名前:
ValidateCustomerAPI
- アプリ登録の種類:
- 「作成」をクリックしてカスタム拡張機能を作成します。
- 作成したカスタム拡張機能の概要ページにある、「API認証」>「必要なアクセス許可」>「アクセス許可の追加」をクリックして管理者の同意を付与します。
- 「External Identities」>「ユーザーフロー」から、
CustomerSignUp
ユーザーフローを開きます。 - 「カスタム認証拡張機能」ページにある「ユーザーが情報を送信したとき」>「何も選択されていません」をクリックします。
- 先ほど作成した
ValidateCustomerAPI
を選択し、「選択」をクリックします。 - 「保存」をクリックします。
動作確認
-
Visual Studioで、
Start Projects
を選択し、デバッグ実行します。 -
「Login」をクリックします。Entra External ID のサインインページが表示されます。
-
「アカウントをお持ちでない場合、作成できます」をクリックします。
-
アカウントに使用するメールアドレスを入力して「次へ」をクリックします。
-
メールアドレス宛に届いた確認コードを入力して「次へ」をクリックします。
-
サインアップページが表示されます。以下の情報を入力して「次へ」をクリックします。
- パスワード: 有効なパスワード
- 利用者ID:
999
(存在しないID) - 表示名: 任意の名前
- 利用規約への同意: チェックを入れる
-
「利用者IDが正しくありません。」というエラーメッセージが表示されることを確認します。
-
利用者IDを
1
(存在するID)に変更して「次へ」をクリックします。 -
サインアップが完了し、Blazor アプリのホームページが表示されることを確認します。
-
「User Claims」ページに移動し、
CustomerID
とTermsOfService
のカスタム属性が表示されていることを確認します。 -
「Home」ページに移動してから、「Logout」をクリックしてサインアウトします。
-
再度「Login」をクリックしてサインインします。
-
本人確認の方法として、電子メール コードを選択します。
-
メールアドレス宛に届いた確認コードを入力して「検証」をクリックします。
-
サインインが完了し、Blazor アプリのホームページが表示されることを確認します。
Discussion