Azure ADのアクセストークンにrolesクレームを入れる方法|OAuth2.0とOIDCの実装手順

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 ステップです。

  1. API アプリ登録(リソース側)を作り、スコープとロールを定義する
  2. クライアントアプリ登録(呼び出し元)に、対象 API とスコープを紐づける
  3. 正しい scope を指定してトークンを要求し、アクセストークンを検証する

API アプリ登録(リソース側)の設定

まずは「呼び出される側」である API を Azure AD に登録します。Web API、Azure Functions、ASP.NET Core API などが該当します。

アプリ登録とアプリケーション ID URI の設定

  1. Azure ポータルで Microsoft Entra ID > アプリの登録 を開きます。
  2. 新しい登録 から API 用のアプリを作成します(例:名前「My API」)。
  3. 登録後、左メニューの 「API の公開(Expose an API)」 を開きます。
  4. アプリケーション ID URI を確認/設定します。
例用途
api://<アプリのクライアントID>デフォルトで自動的に付与される形式
api://contoso.com/myapi独自ドメインを使った分かりやすい形式

途中で URI を変更した場合は、後述する scope の指定もすべて同じ文字列に揃える必要があります。

スコープ(delegated permissions)の定義

次に、「どんな権限をアクセストークンに載せるか」を表すスコープを定義します。

  1. API の公開 > スコープの追加 をクリックします。
  2. スコープ名を指定します(例:CustomClaims)。
  3. ユーザーフレンドリーな表示名と説明を入力します。
  4. 管理者の同意が必要かどうかを選択します(内部アプリなら「必要」にすることが多いです)。

ここで定義したスコープは、最終的にアクセストークンの scp クレームに入ります。

アプリロール(appRoles)の定義

roles クレームを使いたい場合は、API 側に appRoles を定義します。これは「API 内での役割セット」を表し、ユーザーまたはアプリに割り当てることができます。

  1. API アプリのアプリ登録画面で マニフェスト を開きます。
  2. 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 を追加する

  1. クライアントアプリのアプリ登録を開きます。
  2. 左メニューの API のアクセス許可(API permissions) を開きます。
  3. + アクセス許可の追加 をクリックします。
  4. 自分の API(My APIs) から先ほど作成した API アプリを選択します。
  5. 定義しておいたスコープ(例:CustomClaims)を選択します。
  6. 必要に応じて 管理者の同意を与える(Grant admin consent) を実行します。

ここで忘れがちなのが「管理者同意」です。社内利用のアプリケーションでは、ユーザーがサインインしたときに許可ダイアログを出したくないケースが多いため、管理者が事前に同意を実行しておくとスムーズです。

アプリロールの割り当て

appRoles を定義しただけでは、roles クレームは出力されません。ユーザー/グループ/アプリに対して「どのロールを割り当てるか」を設定する必要があります。

  1. Microsoft Entra ID の エンタープライズ アプリケーション を開きます。
  2. 対象の API アプリ(サービスプリンシパル)を選択します。
  3. ユーザーとグループ から、ユーザーまたはグループを追加し、ロール(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 内で判断したい」場合は、一般的には次のようなパターンを取ります。

  1. クライアントが「自前 API 用トークン」を取得する
  2. 自前 API が On-Behalf-Of フローを使って「Graph 用トークン」を取得する
  3. 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 が全く出てこない場合は何を疑えばよい?

次の順で確認するのがおすすめです。

  1. API アプリのマニフェストに appRoles が定義されているか
  2. 対象のユーザー/グループ/アプリにロールを割り当てているか
  3. 取得しているアクセストークンの aud が、ロールを定義した API アプリになっているか
  4. トークンをデコードすると、そもそも 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 を含めた、堅牢で分かりやすい認可ロジックを実現できるはずです。

この記事を書いた人

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

コメント

コメントする

目次