ASP.NET の Web API で RequiredScope("access_as_user") を付けると 403 Forbidden になる──その多くは「呼び出しに使うアクセストークンの scp に必要スコープが入っていない」ことが原因です。トークン確認から Entra ID/B2C 設定、クライアント側の scope 指定まで手順で解説します。
なぜ RequiredScope を付けると 403 Forbidden になるのか
RequiredScope は、API に届いたアクセストークンのクレーム(主に scp)を見て「この呼び出しは必要な委任スコープを持っているか」を判定します。ここで判定に失敗すると、認証(ログイン)は通っていても認可(権限)で落ちるため 403 Forbidden になります。
一方で RequiredScope を外すと 200 OK で通る場合、トークン自体は有効で API に到達できているものの、「スコープが足りない」ことだけが原因になっているケースが多いです。まずはスコープの有無を機械的に確認するのが最短ルートです。
401 と 403 の違いを切り分けに使う
| ステータス | よくある原因 | まず見る場所 |
|---|---|---|
| 401 Unauthorized | トークン無し / 署名不正 / 期限切れ / audience 不一致など「認証」段階の失敗 | Authorization ヘッダー、aud、iss、有効期限、API の Bearer 設定 |
| 403 Forbidden | RequiredScope で要求したスコープがトークンに無い、またはロール/スコープの設計が不一致 | アクセストークンの scp(または roles)と、API 側の要求値 |
最優先: アクセストークンの scp クレームを確認する
「いま API 呼び出しに使っているトークン」に、必要なスコープが入っているかを確認します。ここが曖昧だと、設定をいくら眺めても結論に到達しません。
scp クレームとは
- 委任スコープ(delegated permission)は、アクセストークンの
scpクレームに入るのが基本です。 scpはスペース区切りで複数入ることがあります(例:access_as_user User.Read)。[RequiredScope("access_as_user")]は、scpにaccess_as_userが含まれることを前提に判定します。
roles クレームとの違い(ここでハマる人が多い)
クライアント資格情報(client credentials)などのアプリのみ(app-only)で発行されるトークンでは、scp が無く、代わりに roles(アプリロール)が入ることがあります。この場合、RequiredScope 前提の保護は通りません。
| 呼び出し方式 | トークンに出やすいクレーム | API 側の保護の考え方 |
|---|---|---|
| ユーザーがサインインして呼ぶ(委任) | scp | RequiredScope でスコープを要求する設計が自然 |
| バックエンド同士(アプリのみ) | roles | アプリロール(アプリ権限)で保護する設計が自然 |
トークンの中身を確認する手順
- ブラウザ/クライアント側で API を呼ぶ直前の Authorization: Bearer … を控える
- JWT をデコードして
scpを見る(開発では jwt.ms や手元のデコーダで十分) aud(API の対象)とiss(発行元)も合わせて確認する
チェックすべき代表的なクレーム一覧
| クレーム | 意味 | 確認ポイント |
|---|---|---|
scp | 委任スコープ | access_as_user が入っているか(スペース区切り) |
roles | アプリロール(アプリ権限) | app-only ならこちら。RequiredScope ではなくロール保護が必要 |
aud | トークンの宛先(リソース) | 自分の API の Application ID URI / クライアント ID と一致しているか |
iss | 発行者 | テナント/ポリシー(B2C)の想定と一致しているか |
tid | テナント ID | マルチテナント構成なら想定外のテナントになっていないか |
ver | トークン バージョン | v2.0 前提の実装なら v2 の発行になっているか |
呼び出し元が要求しているスコープ指定を見直す
トークンの scp に access_as_user が無いなら、次は「そもそもクライアントがそのスコープを要求していない」可能性が高いです。特に .default 指定の扱いは誤解が起きやすいポイントです。
「個別スコープ」と「.default」の使い分け
- 委任で API を守るなら、基本は 個別スコープ(例:
api://{api-client-id}/access_as_user)を要求します。 .defaultは「アプリ登録に静的に設定された権限セット」をまとめて要求する仕組みで、主に app-only(client credentials)でよく使われます。- 委任でも使えないわけではありませんが、どの権限がトークンに載るかの理解が曖昧だと、
access_as_userが入らない状態を見落としがちです。
よくある誤り: “見た目はスコープっぽいが、実際に要求していない”
例えば、呼び出し側の設定が次のようになっているとします。
// 例(意図せず .default を使っている)
var scopes = new[] { "api://{api-client-id}/.default" };
この状態で access_as_user を RequiredScope で要求しても、トークンの scp に入らないことがあります。特に「委任スコープとして付与したつもり」でも、同意や設定が未完了だと .default に反映されません。
委任スコープを確実に入れたい場合は、次のようにスコープ値を明示します。
// 推奨: 実際の委任スコープを要求する
var scopes = new[] { "api://{api-client-id}/access_as_user" };
同意が済んだのに scp が増えないときは「トークンキャッシュ」を疑う
Entra ID 側で権限を追加して同意した直後でも、クライアントが古いトークンをキャッシュから再利用していると、scp が増えないまま API を叩き続けます。次を試すと切り分けが早いです。
- 取得処理でスコープを変更して「別トークン」として取り直す(個別スコープを明示する)
- サインアウトしてから再サインインする(キャッシュ破棄)
- 同意画面を再表示する(環境によっては
prompt=consentを使う)
MSAL (.NET) の例: AcquireTokenInteractive / On-Behalf-Of
.NET クライアント(デスクトップ/コンソール/サーバー)で MSAL を使う場合は、取得メソッドに渡す scopes の指定を確認します。
// 例: 対話サインインで API 用のアクセストークンを取得
var result = await pca.AcquireTokenInteractive(new[] { "api://{api-client-id}/access_as_user" })
.ExecuteAsync();
// 例: Web アプリがユーザー トークンを受け取り、API 用に OBO で取得
var resultObo = await cca.AcquireTokenOnBehalfOf(new[] { "api://{api-client-id}/access_as_user" }, userAssertion)
.ExecuteAsync();
SPA (msal-browser) の例: scopes 配列の確認
SPA から直接 API を呼ぶ場合、ログインのための openid/profile と、API 呼び出し用のスコープは別物です。「ログインできた=API のスコープもある」ではありません。
// 例: API 用スコープを要求してアクセストークンを取得
const tokenResponse = await msalInstance.acquireTokenSilent({
scopes: ["api://{api-client-id}/access_as_user"]
});
API 側のアプリ登録で “公開しているスコープ” と一致しているか確認する
RequiredScope は、トークン内の scp の値と API が要求する値が完全一致することを前提にしています。つまり、API のアプリ登録(Entra ID / Azure AD B2C)側で公開しているスコープ定義と、クライアントが要求している文字列がズレていると 403 になります。
一致すべき「3つの名前」を整理する
| 名前 | 例 | どこで使うか |
|---|---|---|
| スコープの値(value) | access_as_user | アクセストークンの scp に入る。RequiredScope に書くのは通常これ |
| スコープの完全名(fully-qualified) | api://{api-client-id}/access_as_user | クライアントがトークン取得時に要求する文字列 |
| Application ID URI | api://{api-client-id} または独自 URI | Expose an API で設定。スコープ完全名の前半になる |
Entra ID / B2C ポータルでの確認ポイント
- API 側のアプリ登録で Expose an API を開き、
access_as_userをスコープとして定義している - クライアント(呼び出し元)アプリ登録の API permissions に、API の Delegated permissions として追加している
- 必要なら Grant admin consent を実施し、同意が完了している
RequiredScopeに渡した文字列が、実際にscpに入る値と 1 文字単位で一致している(大文字小文字、アンダースコア、ハイフン、末尾スペースなど)
B2C を使っている場合の補足
Azure AD B2C では、アプリ ID URI が https://{tenant}.onmicrosoft.com/{api-name} の形で作られることがあります。この場合、クライアントが要求するスコープは次のようになります。
// B2C でよく見る例(環境により異なる)
"https://{tenant}.onmicrosoft.com/{api-name}/access_as_user"
一方でアクセストークンの scp に入るのは通常 access_as_user なので、API 側の RequiredScope は value(短い名前) を使うのが分かりやすいです。
API 側(ASP.NET)の実装と設定を確認する
スコープが正しくても、API 側の認証・認可ミドルウェアが想定通りに動いていないと 403 になります。まずは「アクセストークンを正しく検証し、スコープチェックが適用される」基本形に寄せると、切り分けが一気に楽になります。
最小構成の例(Microsoft.Identity.Web)
// Program.cs(概略)
// using Microsoft.AspNetCore.Authentication.JwtBearer;
// using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
[Authorize] と RequiredScope の基本形
// using Microsoft.AspNetCore.Authorization;
// using Microsoft.Identity.Web.Resource;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class SampleController : ControllerBase
{
[HttpGet]
[RequiredScope("access_as_user")]
public IActionResult Get()
{
return Ok(new { message = "OK" });
}
}
ここで 403 が出る場合は、API に届いたトークンの scp に access_as_user が含まれていない可能性が高いです。逆に、scp が含まれているのに 403 なら、トークンを見ているつもりで別のトークンを投げている(キャッシュ、環境差、呼び出し経路差)など、運用上のズレを疑います。
「実は違うトークンを投げている」パターンを潰す
現場で多いのが「設定は合っているのに scp が無い」ではなく、違う目的のトークンを API に投げているケースです。例えば次のような状態です。
- Microsoft Graph 用に取得したトークンを、そのまま自作 API に投げている(
audが Graph になっている) - サインインのための ID トークンをアクセストークンだと思って送っている(ID トークンには API 用 scp が入りません)
- API A のトークンで API B を呼んでいる(API が複数あると起きがち)
- 開発環境では直っているが、別環境のフロント/バックが古い設定のまま(App Registration が環境で別)
aud が一致しているかで一発判定する
迷ったらまず aud を見てください。aud が自分の API ではないなら、スコープ以前に「そもそもその API 用のトークンではない」可能性が高いです。RequiredScope を外して 200 が返る場合でも、セキュリティ上は危険な状態になり得るので、必ず整合させます。
NuGet パッケージ更新で直ることがある理由
質問者のケースのように、最終的に NuGet パッケージ更新で解消することがあります。これは「スコープ判定ロジックのバグ」だけでなく、認証周りは複数ライブラリの組み合わせで動くため、バージョン不整合が原因で想定と違う挙動になりやすいからです。
更新対象になりやすいパッケージ
Microsoft.Identity.WebMicrosoft.Identity.Web.MicrosoftGraph(使っている場合)Microsoft.Identity.Client(MSAL)Microsoft.AspNetCore.Authentication.JwtBearer(フレームワーク依存で間接更新されることも)
更新時のコツ
- まずは Microsoft.Identity.Web を中心に、メジャーバージョンを揃えて更新する
- 複数プロジェクトがある場合、API とクライアントで参照している Identity 関連パッケージが大きくズレていないか確認する
- 更新後は必ず「キャッシュされたトークン」を捨てて、取り直したトークンで
scpを再確認する
client credentials で呼びたい場合の設計(RequiredScope が通らない理由)
バックエンド同士の呼び出しで、ユーザーを介さない client credentials を使いたい場合、委任スコープの概念がありません。そのため scp による判定である RequiredScope をそのまま使うと 403 になりやすいです。
解決の方向性
- API を「ユーザーが操作する API」と「バックエンドが呼ぶ API」で分け、前者はスコープ、後者はアプリロールで保護する
- もしくは 1 つの API で両方を受けるなら、
scpとrolesのどちらかで通す認可ポリシーを設計する
両対応したいときの考え方(実務的)
| 要件 | おすすめ | 理由 |
|---|---|---|
| ユーザー操作の API だけ守れれば良い | 委任スコープ(scp)で統一 | フロント/ユーザー文脈と相性が良く、最小の設定で済む |
| バックエンド間の API だけ守れれば良い | アプリロール(roles)で統一 | client credentials の正攻法。スコープでは表現しづらい |
| 両方の呼び出しを許可したい | ポリシーで「scp または roles」 | 設計が複雑になるため、最初に要件を固定してから実装すると安全 |
ログで「スコープ不足」を可視化する
403 は原因が 1 つに見えて、実際は「本当に scp が無いのか」「別トークンなのか」「認可設定がずれているのか」で手戻りが増えがちです。開発環境ではログを増やし、判断材料を増やすのが近道です。
- ASP.NET のログレベルを上げて、認証/認可関連のログを出す
- レスポンスヘッダー
WWW-Authenticateにヒントが出ることがあるので確認する - 開発時のみ PII 表示を許可して詳細を追う(本番では無効化)
最短で直すためのチェックリスト
最後に、403 を最短で潰すための「見る順番」をまとめます。上から順に潰すと、無駄な設定いじりが減ります。
| 順番 | やること | 期待する状態 |
|---|---|---|
| 1 | API 呼び出しに使っているトークンをデコードして scp を確認 | access_as_user が入っている |
| 2 | クライアントが要求している scopes を点検(.default ではなく個別スコープ) | api://.../access_as_user を要求している |
| 3 | API 側のアプリ登録でスコープ公開(Expose an API)を確認 | access_as_user が定義済み |
| 4 | クライアント側に委任されたアクセス許可を追加し、同意を完了 | 同意済みでエラー無し |
| 5 | API 側の RequiredScope の値が scp と完全一致しているか確認 | スペル・大小・記号が一致 |
| 6 | Identity 関連 NuGet を最新安定版へ更新し、キャッシュを消して再発行 | 新しいトークンで scp が期待通り |
| 7 | client credentials の場合は roles 保護へ設計変更(または両対応ポリシー) | 委任スコープ前提のズレが解消 |
RequiredScope を付けたとたん 403 になる現象は、原因が分かれば対処はシンプルです。まずは「今投げているアクセストークンの scp に、API が要求する値が入っているか」を機械的に確認し、次にクライアントの scopes 指定とアプリ登録の公開スコープ・同意を揃えます。最後にパッケージ更新とキャッシュクリアまで行えば、再現性のないハマり方も含めて解消しやすくなります。

コメント