Azure AD(Microsoft Entra ID)で OAuth2 / OpenID Connect を使っていると、「アクセストークンにユーザーのロール(roles クレーム)を入れたいのに、どうしても入らない」「スコープを足したら No Access Token Received from Authorization Server と怒られた」という状況によく遭遇します。本記事では、その原因となる仕様の整理から、正しいアプリ登録と設定手順、トラブルシューティングのコツまでを、実務目線で丁寧に解説します。
アクセストークンに roles を入れる前に押さえておくべき前提
まず、「なぜ思ったように roles が入らないのか?」を理解するために、ID トークンとアクセストークンの役割を整理します。ここを曖昧にしたまま試行錯誤すると、設定をいじっても根本原因にたどり着けません。
ID トークンとアクセストークンの違い
| 項目 | ID トークン | アクセストークン |
|---|---|---|
| 主な用途 | クライアントアプリが「誰がサインインしたか」を知る | API が「誰がどの権限で呼び出しているか」を知る |
| aud(対象) | クライアントアプリ | 呼び出される API(リソース) |
| 代表的クレーム | sub, name, preferred_username, email など | aud, scp(スコープ), roles(アプリロール)など |
| クレームの向き | 「ユーザー本人」の情報が中心 | 「API にとって意味がある権限・ロール」が中心 |
アクセストークンには、「そのトークンの aud(audience)に指定されている API に関係する情報だけが入る」という仕様があります。つまり、
- Microsoft Graph 用のアクセストークンには「Graph に対する権限」だけが入る
- 自前 API 用のアクセストークンに自前 API のロールを入れたいなら、自前 API を aud にしたトークンを要求する必要がある
ここが最重要ポイントです。Graph 用トークンに、自分の API の roles クレームを載せることはできません。
なぜ「openid profile」だけではロールが入らないのか
よくある勘違いが「scope=openid profile でトークンを取っているから、ユーザーのロールも入るはず」という考え方です。実際には、openid / profile は 「サインインとユーザープロファイルに関する最低限の権限」 を表すスコープであり、
- ID トークンが返ってくること
- ユーザーの基本情報(name / family_name / given_name など)を取得できること
を保証するだけです。自前 API に対するロールや操作権限は、
- 自前 API 用に定義したスコープ(例:
api://contoso.com/myapi/CustomClaims) - 自前 API に定義したアプリロール(appRoles)
を通じてアクセストークンに反映されます。したがって、
- scope に
openid profileだけを指定 → 自前 API に関する情報は入らない - scope に
openid profile api://.../CustomClaimsを指定 → 自前 API を audience に含めたトークンが返ってくる
という差が生まれます。
Azure AD(Microsoft Entra ID)で roles をアクセストークンに入れる設計方針
ここからは、「どう設定すればアクセストークンに roles(あるいは相当情報)を載せられるのか」を、具体的な手順に落とし込んで解説します。大きく分けると次の 3 ステップです。
- API アプリ登録(リソース側)を作り、スコープとロールを定義する
- クライアントアプリ登録(呼び出し元)に、対象 API とスコープを紐づける
- 正しい scope を指定してトークンを要求し、アクセストークンを検証する
API アプリ登録(リソース側)の設定
まずは「呼び出される側」である API を Azure AD に登録します。Web API、Azure Functions、ASP.NET Core API などが該当します。
アプリ登録とアプリケーション ID URI の設定
- Azure ポータルで Microsoft Entra ID > アプリの登録 を開きます。
- 新しい登録 から API 用のアプリを作成します(例:名前「My API」)。
- 登録後、左メニューの 「API の公開(Expose an API)」 を開きます。
- アプリケーション ID URI を確認/設定します。
| 例 | 用途 |
|---|---|
api://<アプリのクライアントID> | デフォルトで自動的に付与される形式 |
api://contoso.com/myapi | 独自ドメインを使った分かりやすい形式 |
途中で URI を変更した場合は、後述する scope の指定もすべて同じ文字列に揃える必要があります。
スコープ(delegated permissions)の定義
次に、「どんな権限をアクセストークンに載せるか」を表すスコープを定義します。
- API の公開 > スコープの追加 をクリックします。
- スコープ名を指定します(例:
CustomClaims)。 - ユーザーフレンドリーな表示名と説明を入力します。
- 管理者の同意が必要かどうかを選択します(内部アプリなら「必要」にすることが多いです)。
ここで定義したスコープは、最終的にアクセストークンの scp クレームに入ります。
アプリロール(appRoles)の定義
roles クレームを使いたい場合は、API 側に appRoles を定義します。これは「API 内での役割セット」を表し、ユーザーまたはアプリに割り当てることができます。
- API アプリのアプリ登録画面で マニフェスト を開きます。
appRolesセクションを編集し、必要なロールを追加します。
シンプルな例は次のようになります。
"appRoles": [
{
"allowedMemberTypes": [ "User" ],
"description": "読み取り専用ユーザー",
"displayName": "Reader",
"id": "11111111-1111-1111-1111-111111111111",
"isEnabled": true,
"value": "Reader"
},
{
"allowedMemberTypes": [ "User" ],
"description": "管理者ユーザー",
"displayName": "Admin",
"id": "22222222-2222-2222-2222-222222222222",
"isEnabled": true,
"value": "Admin"
}
],
ポイントは次の通りです。
allowedMemberTypesにUserを指定すると、ユーザー/グループに割り当て可能- アプリ(クライアント資格情報フロー)に割り当てたい場合は
Applicationを指定 valueが最終的なrolesクレームの値になる
トークンのクレームをカスタマイズ(オプション)
必要に応じて、「トークン構成(Token configuration)」やマニフェストを使って、
- グループクレーム(groups)を追加する
- 特定の属性(department など)をクレームとして追加する
- 条件付きでクレームをマッピングする
といった細かい調整も可能です。たとえば、実務では「グループは多すぎて JWT が巨大化するので、appRoles だけを使う」という設計もよく取られます。
クライアントアプリ登録(呼び出し元)の設定
次に、React SPA や ASP.NET Core MVC など「トークンを取得して API を呼ぶ側」のアプリ登録を設定します。
API のアクセス許可に自前 API を追加する
- クライアントアプリのアプリ登録を開きます。
- 左メニューの API のアクセス許可(API permissions) を開きます。
- + アクセス許可の追加 をクリックします。
- 自分の API(My APIs) から先ほど作成した API アプリを選択します。
- 定義しておいたスコープ(例:
CustomClaims)を選択します。 - 必要に応じて 管理者の同意を与える(Grant admin consent) を実行します。
ここで忘れがちなのが「管理者同意」です。社内利用のアプリケーションでは、ユーザーがサインインしたときに許可ダイアログを出したくないケースが多いため、管理者が事前に同意を実行しておくとスムーズです。
アプリロールの割り当て
appRoles を定義しただけでは、roles クレームは出力されません。ユーザー/グループ/アプリに対して「どのロールを割り当てるか」を設定する必要があります。
- Microsoft Entra ID の エンタープライズ アプリケーション を開きます。
- 対象の API アプリ(サービスプリンシパル)を選択します。
- ユーザーとグループ から、ユーザーまたはグループを追加し、ロール(Reader / Admin など)を割り当てます。
アプリケーション権限(アプリのみ、クライアント資格情報フロー)で roles を使いたい場合は、クライアントアプリのサービスプリンシパルに対してロールを割り当てます。
トークン要求時の scope の指定
設定ができたら、いよいよトークンを要求します。ここでのミスが「No Access Token Received from Authorization Server」の原因になりがちなので、文字列を一字一句合わせる意識で指定しましょう。
認可リクエストの例(Authorization Code + PKCE)
GET https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
?client_id={client_id}
&response_type=code
&redirect_uri={redirect_uri}
&scope=openid%20profile%20api://contoso.com/myapi/CustomClaims
&code_challenge={code_challenge}
&code_challenge_method=S256
&state=xyz
ポイントは scope パラメーターです。
openid→ OpenID Connect のサインインを行うために必須profile→ ユーザーの基本プロフィール情報を取得するためapi://contoso.com/myapi/CustomClaims→ 自前 API のスコープ
これらを 半角スペース区切りで並べて指定します。
トークンリクエストの例
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
client_id={client_id}&
code={authorization_code}&
redirect_uri={redirect_uri}&
code_verifier={code_verifier}
レスポンスとして ID トークンとアクセストークンが返ってきます。アクセストークンを jwt.ms などでデコードして、次のようなクレームを確認します。
{
"aud": "api://contoso.com/myapi",
"scp": "CustomClaims",
"roles": ["Reader"],
"appid": "{client_id}",
"tid": "{tenant_id}"
}
ここで重要なのは、
audが自前 API の App ID URI になっていることscpに自前 API のスコープ名が入っていること- appRoles を割り当てていれば
rolesに値が入っていること
です。これらが揃っていれば、API 側でトークンを検証し、roles / scp をもとに認可(Authorization)を実装できます。
委任権限とアプリケーション権限での roles / scp の違い
Azure AD のトークンでは、ユーザーあり(委任)とユーザーなし(アプリのみ)の場合でクレームの表現が変わります。よく混乱するポイントなので、一度表で整理しておきましょう。
| シナリオ | ユーザーの有無 | 主に使われるクレーム | 説明 |
|---|---|---|---|
| 委任されたアクセス許可(delegated) | ユーザーあり | scp, 場合により roles | ユーザー+アプリの組み合わせで API を呼び出す。操作粒度は scp に入る。 |
| アプリケーション権限(application) | ユーザーなし | roles | バックエンドバッチなど。アプリに割り当てたロールが roles に入る。 |
自前 API で「ユーザーごとの権限」を制御したい場合、委任権限で scp と roles の両方をうまく使うと設計がシンプルになります。
- scp:
「記事の閲覧」「記事の更新」といった操作粒度の権限(read / write / delete など) - roles:
「一般ユーザー」「管理者」といった職務・役割のセット
このように役割分担をすると、API 内の認可ロジックが読みやすくなります。
「No Access Token Received from Authorization Server」の原因と対処策
自前 API のスコープ(例:api://xxxxxxxxxxx/CustomClaims)を付けた途端に、クライアント側で「No Access Token Received from Authorization Server」と表示されることがあります。これは多くの場合、「トークンエンドポイントまで正しくたどりついていない」あるいは「要求したスコープが不正でトークンが発行されていない」ことが原因です。
原因別のチェックポイント
| よくある原因 | 症状 | 確認ポイント / 対処 |
|---|---|---|
| スコープ文字列の不一致 | 認可コードは取れるが、トークンが返らない or エラー | API の「アプリケーション ID URI」+「スコープ名」と、scope の文字列が完全一致しているかを確認 URI を GUID 形式から独自ドメイン形式に変更した場合、クライアント側の scope も同じに揃える |
| 管理者同意不足 | ユーザーサインイン時に consent 画面でエラー / トークン発行に失敗 | クライアントアプリの「API のアクセス許可」で、対象スコープに対して管理者同意を実施済みか確認 内部アプリなら、事前に admin consent を付与しておくと安心 |
| v2.0 エンドポイントを使っていない | scope を指定しているのに認識されない | エンドポイント URL が /oauth2/v2.0/authorize, /oauth2/v2.0/token になっているか確認 古い v1.0 エンドポイントでは scope ではなく resource パラメータを使う仕様なので混在させない |
| 必須パラメータ不足(PKCE など) | SPA / ネイティブアプリでトークン取得に失敗 | Authorization Code + PKCE の場合、code_challenge と code_verifier が正しく対応しているか redirect_uri がアプリ登録の設定と完全一致しているか response_type=code を指定しているか |
| テナントや発行元の不整合 | マルチテナント環境で謎のエラー | テナント ID / common / organizations などの指定が設計通りか パブリッシャードメインとリダイレクト URI のドメインに矛盾がないか |
実際のトラブルシューティングでは、ブラウザのネットワークタブや開発者ツールのログを確認し、トークンエンドポイントのレスポンスで返ってくる error / error_description を見るのが近道です。「No Access Token Received from Authorization Server」というフロント側のメッセージだけでは、根本原因までは分からないためです。
Graph 用トークンと自前 API 用トークンを混同しない
Azure AD を使っていると、「Microsoft Graph も自前 API も同じユーザー情報を扱うから、トークンも一つで済ませたい」と考えがちですが、ここには仕様上の限界があります。
- Graph 用アクセストークンの aud は
https://graph.microsoft.com - 自前 API 用アクセストークンの aud は
api://contoso.com/myapiなど
この 2 つは「完全に別のトークン」です。Graph 用トークンに自前 API の roles を入れることはできませんし、その逆もできません。
もし「ユーザーが持つ Graph の情報(所属グループ、マネージャーなど)を見て API 内で判断したい」場合は、一般的には次のようなパターンを取ります。
- クライアントが「自前 API 用トークン」を取得する
- 自前 API が On-Behalf-Of フローを使って「Graph 用トークン」を取得する
- API 内で Graph を呼び出し、ユーザー情報やグループ情報を参照する
このように、「audience ごとにトークンを分けて設計する」のが正攻法です。
API 側での認可実装パターン
アクセストークンに roles や scp が入るようになったら、次は API 側でこれをどう使うかです。ここでは代表的なパターンを簡単に紹介します。
ASP.NET Core Web API の例
ASP.NET Core では、ポリシーベースの認可を使って roles / scopes を判定するのが一般的です。
services.AddAuthorization(options =>
{
options.AddPolicy("RequireReaderRole", policy =>
policy.RequireClaim("roles", "Reader"));
options.AddPolicy("RequireCustomClaimsScope", policy =>
policy.RequireClaim("scp", "CustomClaims"));
});
コントローラー側では次のように指定します。
[Authorize(Policy = "RequireReaderRole")]
[HttpGet("items")]
public IActionResult GetItems()
{
// Reader ロールがあるユーザーだけがアクセス可能
}
あるいは scope ベースで判定することもできます。
[Authorize(Policy = "RequireCustomClaimsScope")]
[HttpPost("items")]
public IActionResult CreateItem()
{
// CustomClaims スコープを持つユーザーだけがアクセス可能
}
このように、「roles は大まかな役割」「scp は実行したい操作の粒度」として分けて使うと、権限設計が明確になります。
大規模テナントでのロール/グループ設計のコツ
ユーザーやグループの数が多いテナントでは、「JWT が巨大になりすぎる」「グループ ID が山ほど入ってしまう」といった問題も出てきます。ここでは、実務でよく採用される設計のコツをいくつか挙げます。
- アプリロール(appRoles)を基本とし、グループクレームの付与は最小限にする
- 組織の役割に対応した appRoles(Reader / Editor / Admin など)を定義し、グループ単位でロールを割り当てる
- API 内では roles クレームだけを見て判断し、グループの細かい違いは別の仕組み(データベース、設定ファイルなど)で吸収する
もしグループ情報のクレームがどうしても必要で、JWT のサイズが問題になる場合は、_claim_names / _claim_sources を使った「グループの省略表記」や、API 側から Graph API を呼び出して必要なときだけグループを取得する方式も検討できます。
よくある質問とその回答(FAQ)
ID トークンには roles が入るのに、アクセストークンには入らないのはなぜ?
クライアントアプリに appRoles を割り当てた場合、ID トークンに roles が入ることがあります。しかし、アクセストークンに出る roles は「audience(呼び出される API)」に対して割り当てた appRoles のみです。つまり、
- ID トークンの roles → クライアントアプリに対して割り当てられたロール
- アクセストークンの roles → aud に指定された API に対して割り当てられたロール
となるため、「どのアプリに対するロールか」を意識する必要があります。
roles が全く出てこない場合は何を疑えばよい?
次の順で確認するのがおすすめです。
- API アプリのマニフェストに appRoles が定義されているか
- 対象のユーザー/グループ/アプリにロールを割り当てているか
- 取得しているアクセストークンの aud が、ロールを定義した API アプリになっているか
- トークンをデコードすると、そもそも roles クレームがないのか、空なのか
特に最後の 2 つが見落とされがちです。「Graph 用トークンを見て roles が無い」と言いながら、実は自前 API のロールを探していた、というケースもよくあります。
roles と scp のどちらを優先的に使えばいい?
これは設計方針によりますが、次のように使い分けるとわかりやすくなります。
- 小規模・シンプルな API:スコープ(scp)だけで権限を表現してもよい
- 中〜大規模:roles で「役割」、scp で「操作」を表現し、両方を組み合わせる
- バックエンドバッチ:アプリケーション権限で roles を使う
どちらか一方だけにこだわるというより、「roles=誰」「scp=何を」の 2 軸で整理すると、運用しやすい設計になります。
まとめ:アクセストークンに roles を入れるためのチェックリスト
最後に、本記事の内容を「実装時に確認すべきチェックリスト」として整理しておきます。新規実装でもトラブルシューティングでも、この項目を順に潰していけば、ほとんどの問題は解消できます。
- アクセストークンの aud が自前 API の App ID URI になっているか
- scope に
openid profileだけでなく、自前 API のスコープ(api://.../CustomClaimsなど)を指定しているか - API アプリ側でスコープと appRoles を定義しているか
- ユーザー/グループ/アプリに appRoles を割り当てているか
- クライアントアプリの「API のアクセス許可」で対象 API を追加し、管理者同意を与えているか
- v2.0 エンドポイント(
/oauth2/v2.0/authorize,/oauth2/v2.0/token)を利用しているか - トークンを jwt.ms などでデコードし、aud / scp / roles を実際に確認したか
- Graph 用トークンと自前 API 用トークンを混同していないか
この流れで設計・実装すれば、「No Access Token Received from Authorization Server」に悩まされる時間は大幅に減り、アクセストークンに期待通りの roles / scp を含めた、堅牢で分かりやすい認可ロジックを実現できるはずです。

コメント