Blazor Server と Microsoft Entra ID(旧 Azure AD)でサインインは成功するのに roles クレームが取れず、ロールベース認可が常に失敗する――この“あるある”を、原因の切り分けから具体的な修正コード、Azure ポータルのどこを触るかまで一気に解決できるよう、実運用の観点で徹底整理しました。読み終わる頃には、Principal にロールが確実に載る状態を自信を持って再現できるはずです。
質問概要
- Blazor Server アプリで Microsoft.Identity.Web を使用し、認証プロバイダーは Microsoft Entra ID(旧 Azure AD)。
- サインインは成功するが、サインイン直後の
ClaimsPrincipalにユーザーの roles クレームが含まれず、[Authorize(Roles = "...")]が常に失敗。 - 既に以下は実施済み:
- アプリ ロール(App roles)の登録
- ユーザー/グループをロールに割り当て
- 24 時間以上待機
- 追加で確認・設定すべき点は何か。
結論(最短の答え)
多くのケースは、割り当ての場所(「エンタープライズ アプリ」側)と、ASP.NET 側のロールクレーム型(RoleClaimType)の不一致で説明できます。以下の 2 点をまず実施してください。
- Azure ポータルのエンタープライズ アプリ(サービス プリンシパル)でユーザー/グループをロールに割り当て直す。
Program.csでOpenIdConnectOptions.TokenValidationParameters.RoleClaimType = "roles"を明示する。
これで大半の「roles が来ない」事象は解消します。以降は、失敗しないための詳細な手順と、例外パターンの網羅、デバッグの勘所まで一気に解説します。
回答・解決策(チェックリスト)
| 手順 | 内容 | 補足 |
|---|---|---|
| 1 | アプリ ロール設定を確認 Azure ポータル → アプリ登録 → 対象アプリ → アプリ ロール で allowedMemberTypes が “User” を含むことを確認。 | “Application” のみだとユーザーにロールが付与されない。 |
| 2 | 割り当ては「エンタープライズ アプリ」側で実施 Azure ポータル → エンタープライズ アプリ → 対象アプリ → ユーザーとグループ からロールを割り当てる。 | 「アプリ登録」側での割り当ては実トークンに反映されない。 |
| 3 | 管理者による同意 (Admin Consent) を付与 エンタープライズ アプリ → アクセス許可 で Grant admin consent for <組織名> を実行。 | アプリがディレクトリ データ等を読む権限を要求している場合、同意がないとクレームが発行されないことがある。UI 変化がなくても効果は即時。 |
| 4 | roles クレームのマッピングを明示Program.cs で TokenValidationParameters.RoleClaimType = "roles" を指定。 | .NET は既定で "role" または ClaimTypes.Role を見る。Entra は "roles" を発行。 |
| 5 | キャッシュをクリアして再サインイン ブラウザー Cookie/セッションを削除、または InPrivate/シークレット ウィンドウで再ログイン。 | 古い ID トークンには新たなクレームが入らない。 |
| 6 | ID トークンの中身を確認Microsoft.Identity.Web のログを有効化し、ID トークンを jwt.ms 等でデコード。 | Blazor Server はトークンをサーバー側に保存。ブラウザー開発者ツールからは直接見えない。 |
なぜ roles が来ないのか(根本理解)
Entra ID の「アプリ ロール」は、アプリ登録(アプリケーション)の定義と、エンタープライズ アプリ(サービス プリンシパル)での割り当てという 2 面体で成立しています。ユーザーに実際のロールを付与するのは後者です。ここでの割り当てが欠けていると、サインインは成功しても roles は発行されません。
また、ASP.NET Core 側はロールの既定クレーム型を ClaimTypes.Role(URI 形式)と見ることがあります。Entra ID は roles という 配列クレームを発行するため、RoleClaimType を "roles" に合わせないと、Principal の IsInRole() や [Authorize(Roles="...")] でロールが判定されません。
Azure ポータルでの具体設定
アプリ ロール定義の見直し(アプリ登録)
アプリ登録 → マニフェストの appRoles には、少なくとも allowedMemberTypes に "User" を含むエントリを置きます。例:
"appRoles": [
{
"allowedMemberTypes": [ "User" ],
"displayName": "レポート管理者",
"id": "11111111-2222-3333-4444-555555555555",
"isEnabled": true,
"description": "レポート機能の管理を許可",
"value": "Report.Admin"
},
{
"allowedMemberTypes": [ "User" ],
"displayName": "レポート閲覧者",
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"isEnabled": true,
"description": "レポートの閲覧を許可",
"value": "Report.Reader"
}
]
- Authorize 属性が比較するのは
valueの文字列です(例:Report.Admin)。displayNameではありません。 - ユーザーではなくアプリケーション(機密クライアント)に付与するロールは
allowedMemberTypesを"Application"にしますが、人間ユーザーの認可には"User"が必須です。
ユーザー/グループへのロール割り当て(エンタープライズ アプリ)
- エンタープライズ アプリ → 対象アプリ → ユーザーとグループ
- 追加 → 対象ユーザー/グループを選択 → 右側でロール(例:
レポート管理者)を選択 → 割り当て
ここで割り当てた内容がサインイン時の roles クレームに反映されます。マルチテナント構成の場合は、ユーザーが所属するテナント側の「エンタープライズ アプリ」で割り当てる点に注意してください。
管理者同意(必要な場合)
アプリが Microsoft Graph などの権限を要求している場合、管理者同意がないとトークンに必要なクレームが出ないケースや、サインイン後の API 呼び出しが失敗して結果的にロール判定に到達しないケースがあります。
エンタープライズ アプリ → アクセス許可 → Grant admin consent for … を実行しておきましょう。
ASP.NET Core(Blazor Server)側の確実な設定
Program.cs(.NET 8 例)
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Web;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// 認証
builder.Services
.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi() // 下流 API 呼び出しをする場合
.AddInMemoryTokenCaches();
// ★ ここが重要:roles を ASP.NET のロールとして扱う
builder.Services.Configure(
OpenIdConnectDefaults.AuthenticationScheme,
options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles", // ← これがないと [Authorize(Roles=...)] が効かない
NameClaimType = "name" // 任意:UI の表示名
};
options.SaveTokens = true; // デバッグや下流呼び出しで便利
});
// 認可ポリシー(例)
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ReportAdminsOnly",
policy => policy.RequireRole("Report.Admin"));
});
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// パイプライン順序に注意:認証 → 認可
app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
Razor/Blazor 側の使い方
[Authorize(Roles = "Report.Admin")] をページやコンポーネントに付けるか、<AuthorizeView Roles="Report.Admin"> を使います。
// ページやコンポーネントの属性
@attribute [Authorize(Roles = "Report.Admin")]
<AuthorizeView Roles="Report.Admin">
<Authorized>
<p>管理者向けコンテンツ</p>
</Authorized>
<NotAuthorized>
<p>権限がありません。</p>
</NotAuthorized>
</AuthorizeView>
「それでもダメ」の時に効く診断の順番
- 新しい ID トークンを発行させる:InPrivate ウィンドウでログインし直し。ポータル設定変更後も、既存セッションでは古いトークンを持ち続けます。
- Principal の中身をダンプ:
OnTokenValidatedで受け取る ID トークンのクレームをログ出力。builder.Services.Configure<OpenIdConnectOptions>( OpenIdConnectDefaults.AuthenticationScheme, options => { options.Events = new OpenIdConnectEvents { OnTokenValidated = ctx => { var claims = string.Join(", ", ctx.Principal!.Claims.Select(c => $"{c.Type}={c.Value}")); ctx.HttpContext.RequestServices .GetRequiredService<ILoggerFactory>() .CreateLogger("ClaimsDump") .LogInformation("Claims: {claims}", claims); return Task.CompletedTask; } }; }); - トークン自体をデコード:
options.SaveTokens = trueを有効にした上で、受け取った ID トークンをjwt.msなどで確認し、rolesが入っているかを切り分け。 - ロール名の不一致を疑う:
[Authorize(Roles="Report.Admin")]の「Report.Admin」はappRoles[].valueと一致していますか?displayNameではありません。 - パイプライン順序:
app.UseAuthentication()より前にUseAuthorization()を書いていないか、または片方を呼んでいない、という凡ミスを除外。 - マルチテナントの割り当て先:ユーザーがいるテナントの「エンタープライズ アプリ」でロールを付けましたか? アプリ登録元テナントではなく、実ユーザーの所属テナント側で割り当てが必要です。
よくある誤解/落とし穴(実例集)
- 「オプション クレーム」で
rolesを追加する必要がある?
不要です。アプリ ロールを定義して割り当てれば、ID トークンにrolesは自動的に入ります。 - 「アプリ登録」画面のユーザー割り当て欄だけで十分?
不十分です。エンタープライズ アプリでの割り当てが本命。 - グループ割り当てなら
groupsクレームを見ればよい?
グループをロールに割り当てるのは OK ですが、アプリ側は ロールベースの認可を行うべきです。groupsはグループ ID の列挙であり、[Authorize(Roles=...)]の判定には使われません。 - トークンが大きすぎる問題
極端に多くのグループやロールを付けるとトークンサイズ制限に触れることがあります。不要なロールの片付け、グループの整理、またはアプリ側での権限最小化を検討してください。 - Entra ID B2C を使っている
B2C はクレーム名や拡張属性が異なります(extension_で始まることが多い)。本記事の手順は「Entra ID(商用組織テナント)」が対象です。
実装のバリエーション(高度な選択肢)
クレーム変換で「roles → Role」へコピー
レガシー実装や複合的なクレーム設計では、クレーム変換で roles を ClaimTypes.Role にコピーして互換性を保つ選択もあります。
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
public sealed class RolesToRoleClaimTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var id = (ClaimsIdentity)principal.Identity!;
var hasRoleClaims = id.FindFirst(ClaimTypes.Role) != null;
if (!hasRoleClaims)
{
foreach (var role in principal.FindAll("roles"))
{
id.AddClaim(new Claim(ClaimTypes.Role, role.Value));
}
}
return Task.FromResult(principal);
}
}
// DI 登録
builder.Services.AddScoped<IClaimsTransformation, RolesToRoleClaimTransformation>();
ポリシーベース認可での柔軟な表現
ロールに加えて別クレーム条件も併用したい場合は、ポリシーで表現します。
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ReportAdminsInHQ", policy => policy
.RequireRole("Report.Admin")
.RequireClaim("office", "HQ"));
});
テストの必勝パターン(ミニ QA)
- どのトークンを見ればよい?
認可属性の判定は、サインインで得たID トークン由来のクレームに基づきます。アクセス トークン(API 呼び出し用)ではありません。 - ロールの日本語名は使える?
displayNameは日本語でも問題ありません。ただしアプリが参照するのはvalueです。valueは英数字+ドットなどの安定識別子にしましょう。 - ロール変更の反映タイミングは?
新しい ID トークンを取得した時点で反映されます。サインインし直すのが確実です。 - Entra の UI が変わって迷子になる
名称変更は頻繁です。「アプリ登録」「エンタープライズ アプリ」「ユーザーとグループ」「アプリ ロール」の 4 つの言葉を頼りに探すと最短で辿り着けます。
運用テンプレート(手戻りゼロの作業手順書)
- ロール設計:
value(例:Report.Admin)を先に確定。 - アプリ ロールの定義:アプリ登録 → マニフェスト(または UI)で
appRolesを作成。allowedMemberTypesに"User"を含める。 - 割り当て:エンタープライズ アプリ → ユーザーとグループでユーザー/グループを該当ロールに割り当て。
- アプリ側設定:
RoleClaimType = "roles"、UseAuthentication()→UseAuthorization()の順。 - 検証:InPrivate で再ログインし、
jwt.ms等でrolesを確認。ページに<AuthorizeView Roles="...">を置いて視覚確認。 - ログとモニタリング:
Microsoft.Identity.WebのログをDebug以上にし、クレームダンプを残す。
実践的なサンプル一式
appsettings.json(抜粋)
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "contoso.com",
"TenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"ClientId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"ClientSecret": "(機密情報)",
"CallbackPath": "/signin-oidc"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Identity.Web": "Debug"
}
}
}
UI 側の最小確認コード
@page "/whoami"
@using System.Security.Claims
@inject AuthenticationStateProvider Auth
現在のユーザー情報
@code {
private IEnumerable _claims = Enumerable.Empty();
protected override async Task OnInitializedAsync()
{
var state = await Auth.GetAuthenticationStateAsync();
_claims = state.User.Claims.OrderBy(c => c.Type);
}
}
@foreach (var c in _claims)
{
@c.Type = @c.Value
}
ここに roles が並べば、ASP.NET 側設定は正しく、残る問題は Azure 側(割り当て)であると切り分けられます。
追加のテクニックと注意点
- ロールの命名規則:
機能.権限(例:Report.Admin)などドット区切りにすると、ポリシーや UI の記述がすっきりします。 - ロールの過剰発行を避ける:ロールを増やしすぎると運用が破綻します。まずは「管理者」「閲覧者」「承認者」程度から始め、必要性が明確になったら分割する方針が楽です。
- アクセス制御の粒度:ロールで「領域」を、クレームで「属性(部署、拠点、顧客 ID)」を表すと拡張性が高い設計になります。
- 失権のテスト:ロールを外した時に即時で非表示/アクセス拒否になるか、InPrivate での再ログインを含めて確認しましょう。
トラブルシュート早見表
| 症状 | 推定原因 | 対処 |
|---|---|---|
トークンに roles が無い | 割り当てを「エンタープライズ アプリ」で行っていない | サービス プリンシパル側でユーザー/グループにロール割り当て |
トークンに roles はあるが [Authorize] が効かない | RoleClaimType が roles になっていない | TokenValidationParameters.RoleClaimType = "roles" を設定 |
| 一部ユーザーだけ失敗 | 割り当て漏れ/マルチテナントの割り当て先ミス | 対象ユーザーの所属テナントの「エンタープライズ アプリ」での割り当てを確認 |
| 設定を変えても反映されない | 古いセッショントークンを使っている | ブラウザーの Cookie をクリア、InPrivate で再ログイン |
| グループをロールに割り当てたのに効かない | アプリ側でグループ ID による判定をしている | [Authorize(Roles="...")] のロール判定を使う |
Blazor Server ならではのポイント
- サインインの瞬間に Principal が確定:以後は SignalR の接続中でも同じ Principal を使います。ロール変更時は再サインインが必要です。
- <AuthorizeView> の活用:UI の出し分けとルート保護の双方でロールを評価しましょう(UI だけの制御にしない)。
- 下流 API 呼び出し:Microsoft Graph などを呼ぶ場合、アクセス トークンにはロールではなくスコープが入ります。アプリ内の認可(ロール)と API 認可(スコープ)を混同しないこと。
最後の確認(ゼロから 5 分で通す手順)
- アプリ登録で
appRolesを定義(valueを確定)。 - エンタープライズ アプリでユーザー/グループにロールを割り当て。
Program.csにRoleClaimType = "roles"を追記。- InPrivate でログインし、
/whoamiのような確認ビューでrolesを視認。 - ページやコンポーネントに
[Authorize(Roles="...")]または<AuthorizeView Roles="...">を適用。
まとめ
Entra ID のアプリ ロールは定義(アプリ登録)と割り当て(エンタープライズ アプリ)の二段構えです。さらに ASP.NET 側で RoleClaimType = "roles" を明示すれば、Blazor Server のロールベース認可は安定して動作します。この記事の手順に沿って設定すれば、「サインインは通るのにロールが無い」というよくあるハマりどころを確実に回避でき、Principal にロールが取り込まれて正しく [Authorize] が判定されるはずです。
付録:参考コード(.NET 6/7 でも応用可)
// .NET 6/7 でも AddMicrosoftIdentityWebApp と Configure<OpenIdConnectOptions> の考え方は同じ。
// 既存の Startup.cs 構成でも、ConfigureServices の中で RoleClaimType を "roles" にしておけば動作します。
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"));
services.Configure(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles"
};
});
補足:チェック用スニペット(誰でもすぐに埋め込める)
<AuthorizeView Roles="Report.Admin">
<Authorized><span>(Admin)</span></Authorized>
<NotAuthorized><span>(一般)</span></NotAuthorized>
</AuthorizeView>
この表示が期待通りに切り替われば、ロール付与 → トークン発行 → Principal への取り込み → UI 評価までのパイプラインが整っていることの確かな証拠になります。
現場メモ(運用ノウハウを 3 行で)
- ロールは value を API・コードの安定識別子として使い、displayName は人間可読に割り当てる。
- 設定変更後は必ず InPrivate で再サインイン。トークンは思いのほか粘る。
- 迷ったら「エンタープライズ アプリの割り当て」「RoleClaimType=roles」「古いトークン破棄」の 3 点セットを確認。

コメント