ASP.NET Web API の RequiredScope で 403 Forbidden になる原因と解決策|access_as_user スコープの確認方法

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 ForbiddenRequiredScope で要求したスコープがトークンに無い、またはロール/スコープの設計が不一致アクセストークンの 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 側の保護の考え方
ユーザーがサインインして呼ぶ(委任)scpRequiredScope でスコープを要求する設計が自然
バックエンド同士(アプリのみ)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 URIapi://{api-client-id} または独自 URIExpose 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.Web
  • Microsoft.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 を最短で潰すための「見る順番」をまとめます。上から順に潰すと、無駄な設定いじりが減ります。

順番やること期待する状態
1API 呼び出しに使っているトークンをデコードして scp を確認access_as_user が入っている
2クライアントが要求している scopes を点検(.default ではなく個別スコープ)api://.../access_as_user を要求している
3API 側のアプリ登録でスコープ公開(Expose an API)を確認access_as_user が定義済み
4クライアント側に委任されたアクセス許可を追加し、同意を完了同意済みでエラー無し
5API 側の RequiredScope の値が scp と完全一致しているか確認スペル・大小・記号が一致
6Identity 関連 NuGet を最新安定版へ更新し、キャッシュを消して再発行新しいトークンで scp が期待通り
7client credentials の場合は roles 保護へ設計変更(または両対応ポリシー)委任スコープ前提のズレが解消

RequiredScope を付けたとたん 403 になる現象は、原因が分かれば対処はシンプルです。まずは「今投げているアクセストークンの scp に、API が要求する値が入っているか」を機械的に確認し、次にクライアントの scopes 指定とアプリ登録の公開スコープ・同意を揃えます。最後にパッケージ更新とキャッシュクリアまで行えば、再現性のないハマり方も含めて解消しやすくなります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次