OAuth 2.0 の記事と認可コードフローの記事で、アクセストークンは「範囲と期限のある通行証」だと書きました。この「範囲」にあたるのがスコープです。scope= のパラメータや、同意画面に並ぶ許可の一覧、トークンの中に入る scp(スコープを表す項目)など、スコープはあちこちに顔を出します。この記事では、スコープがどこで決まり、誰が見て判断しているのかを、RFC と Microsoft のドキュメントで確かめながら図にしました。
- スコープは「このアクセストークンで何をしてよいか」の範囲
- スコープの書き方(RFC の決まりと、Microsoft の「リソース+許可の名前」)
- 頼んだ範囲がそのまま通るとは限らないこと、トークンは API ごとに1枚であること
- API はトークンの中のスコープの情報(scp・roles)を見て判断し、足りなければ 403 を返すこと
- スコープは最小限に、必要になったときに少しずつ頼むこと
まずはたとえで:開けられる扉の一覧
Microsoft Graph のような API は、「メールを読む」「メールを送る」「予定表を読む」のように、できることを細かく分けています。アクセストークンには、そのうちどの扉を開けてよいかの一覧が付いています。この一覧がスコープです。
RFC 6749 も、クライアントは認可エンドポイントやトークンエンドポイントで、アクセスの範囲(scope)を指定できると書いています。Microsoft のドキュメントでは、API が許可を細かく分けておくことで、アプリは自分に必要な許可だけを頼めるようになり、利用者や管理者も「このアプリが何にアクセスできるか」を分かったうえで許可できる、と説明しています。OAuth 2.0 ではこの許可を「スコープ」と呼び、Microsoft では「アクセス許可(permission)」と呼ぶことも多いです。
スコープはどこに出てくるか
スコープは、アプリが頼んでから API が判断するまで、形を変えながら流れていきます。
| 場所 | スコープの姿 |
|---|---|
| ① 認可リクエスト | アプリが scope= のパラメータで「この範囲を許してほしい」と頼む |
| ② 同意画面 | 求めている許可が一覧で出る。スコープの文字列そのものではなく、説明文で表示される |
| ③ トークンの応答 | scope の項目で、実際に許された範囲が返る |
| ④ トークンの中 | Microsoft の場合、人の代わりなら scp、アプリ自身なら roles という項目(クレーム)として入る。くわしくはこの節の最後で説明する |
| ⑤ API | トークンの scp や roles を見て、その操作をしてよいかを判断する |
①と③は、認可コードフローの記事で見た Entra ID の例にも出てきます。
① 認可リクエストの scope(Microsoft のドキュメントより)
// Line breaks for legibility only
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?
client_id=00001111-aaaa-2222-bbbb-3333cccc4444
&response_type=code
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
&response_mode=query
&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read
&state=12345
&code_challenge=YTFjNjI1OWYzMzA3MTI4ZDY2Njg5M2RkNmVjNDE5YmEyZGRhOGYyM2IzNjdmZWFhMTQ1ODg3NDcxY2Nl
&code_challenge_method=S256
③ トークンの応答の scope(Microsoft のドキュメントより)
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q...",
"token_type": "Bearer",
"expires_in": 3599,
"scope": "https%3A%2F%2Fgraph.microsoft.com%2Fmail.read",
"refresh_token": "AwABAAAAvPM1KaPlrEqdFSBzjqfTGAMxZGUTdM0t4B4...",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJhdWQiOiIyZDRkMTFhMi1mODE0LTQ2YTctOD..."
}
どちらも URL エンコードされているので読みにくいですが、元に戻すと https://graph.microsoft.com/mail.read、つまり「Microsoft Graph でメールを読む」という意味です。
トークンの中の scp と roles とは
ここまでに何度か出てきた scp と roles は、アクセストークンの中に入っている項目の名前です。トークンは、「宛先はどの API か」「誰のトークンか」「何が許されたか」といった情報をいくつも持っています。Microsoft のドキュメントでは、この1つ1つの情報をクレームと呼んでいます。
- scp:スコープ(scope)のクレームです。人の代わりに API を呼ぶ(委任の)トークンに入り、アプリが頼んで同意を得たスコープが、
Mail.Read User.Readのようにスペース区切りで並びます。 - roles:アプリロールのクレームです。人がいないクライアントクレデンシャルでは、スコープの代わりにこちらを使います。アプリに与えられたアプリケーションのアクセス許可が、一覧で入ります。アプリ自身のトークンには
scpは入りません。
API は、受け取ったトークンのこの2つのクレームを見て、「このトークンでこの操作をしてよいか」を判断します。図3の値は説明のための例で、実際のトークンにはほかにも多くのクレームが入っています。なお、OAuth 2.0 の記事で書いたとおり、トークンの中身を読んで確かめるのは API の役目で、アプリはトークンを読まずにそのまま運びます。
スコープの書き方
RFC 6749 の決まり
RFC 6749 が決めているのは、書き方の枠組みだけです。
- スペースで区切って並べる:スコープの値は、スペースで区切った文字列の並びです。並び順は関係ありません。
- 大文字と小文字を区別する:同じつづりでも、大文字と小文字が違えば別の値です。
- 意味は認可サーバが決める:どんな文字列があり、何を許すのかは、認可サーバ(とその先の API)が決めます。OAuth 2.0 の仕様が「メールを読む」のような中身を決めているわけではありません。
なお、RFC 6750 は、API がエラーで返すスコープの値について、プログラムが使うためのもので、利用者に見せるためのものではないと書いています。同意画面にスコープの文字列ではなく説明文が出るのも、このためです。
Microsoft での書き方
ここからは Microsoft の場合です。Microsoft では、スコープをリソース(API)の識別子と許可の名前をつなげて書きます。
| スコープの例 | 意味 |
|---|---|
| https://graph.microsoft.com/Calendars.Read | Microsoft Graph で、利用者の予定表を読む |
| User.Read | リソースを省くと Microsoft Graph と見なされる。https://graph.microsoft.com/User.Read と同じ |
| api://<クライアント ID>/api.read | 自社の API の例。アプリ ID URI と、API の登録で決めたスコープ名をつなげる |
| openid・email・profile | OpenID Connect のスコープ。ID トークンや、メールアドレス・名前などの情報を受け取る |
| offline_access | リフレッシュトークンを受け取る。同意画面では「アクセスを許可したデータへのアクセスを維持する」という意味の項目として出る |
許可の名前には、Files.Read.All のように「対象.操作.制約」の形で付ける慣例があります。名前を見れば、何に対して何をする許可なのか、おおよそ見当がつきます。
スコープの名前と書き方は、API の持ち主が決める
ここまでで分かるとおり、規格が決めているのは書き方の枠組みだけです。「メールを読む」を Mail.Read と書くか、別の名前にするか、リソースの識別子を付けるかどうかは、規格の外の話です。スコープの名前と書き方を決めているのは、その API(リソース)の持ち主です。
- OAuth 2.0(RFC 6749):スペースで区切る、大文字と小文字を区別する、意味は認可サーバが決める、という枠組みだけを決めています。
- OpenID Connect:
openid・profile・email・offline_accessなどは OpenID Connect の仕様で決まっているので、認可サーバが違っても共通です。ただし、どこまで対応しているかは認可サーバによります(Microsoft はaddressとphoneには対応していません)。 - API の持ち主:それ以外のスコープの名前と書き方を決めます。同じ Microsoft の中でも、API によって違うことがあります。Microsoft の MSAL のドキュメントも、スコープの書き方はトークンを受け取る API と、その API が受け付ける宛先(
aud)によって変わると書いています。
| スコープの例(Microsoft のドキュメントより) | 書き方の特徴 |
|---|---|
| https://graph.microsoft.com/User.Read | Microsoft Graph。User.Read だけでも同じ意味になる |
| https://management.core.windows.net//user_impersonation | Azure Resource Manager。宛先の末尾にスラッシュが付くため、スラッシュを2つ重ねる |
| 00001111-aaaa-2222-bbbb-3333cccc4444/api.read | https:// などを付けず、アプリの ID とスコープ名だけを求める API もある |
そのため、アプリの開発者は、呼びたい API のドキュメント(Microsoft Graph ならアクセス許可の一覧)でスコープの名前と書き方を調べ、それをアプリのコードや設定に書きます。Microsoft の場合は、使う許可をアプリ登録にも登録しておきます。
逆に、自社の API を Entra ID で守るときは、自分がスコープを決めて公開する側になります。Expose an API でアプリ ID URI とスコープ名を登録し、その名前を API を使うアプリの開発者に伝え、API のコードでトークンの scp を見て判断します。
頼んだ範囲がそのまま通るとは限らない
アプリが頼んだスコープは、あくまで「希望」です。最終的にどこまで許すかは、認可サーバが決めます。
- 一部または全部を外せる:RFC 6749 は、認可サーバが、自分の方針や持ち主の判断によって、頼まれたスコープの全部または一部を無視できるとしています。
- 違うときは必ず知らせる:出したスコープが頼まれたものと違うときは、トークンの応答に
scopeを入れて、実際に許した範囲を知らせなければなりません。 - 省いたとき:アプリがスコープを指定しなかったときは、あらかじめ決めた既定の範囲で進めるか、エラーにするかのどちらかです。
Microsoft の場合、強い権限のスコープは管理者しか同意できません。たとえば User.Read.All(全ユーザーのプロフィールを読む)、Directory.ReadWrite.All(ディレクトリに書き込む)、Group.Read.All(全グループを読む)です。組織のユーザーがこれらを頼むと、同意できない旨のエラーになります。どのスコープに管理者の同意が要るかは、API を作った側が決めるほか、テナントの管理者が設定で厳しくすることもできます。そのため、アプリはトークンが取れない場合にも備えておく必要があります。
トークンは API ごとに1枚
Microsoft の場合、1つのアクセストークンで呼べるのは1つの API だけです。では、Microsoft Graph と自社の API の両方を呼ぶアプリは、どうするのでしょうか。流れを並べると、まとめて1回で済む部分と、API ごとに別々になる部分に分かれます。
| 部分 | 手順 | 何が起きるか |
|---|---|---|
| まとめて1回(同意) | ①〜④ | /authorize では、複数の API のスコープを1回の要求に並べられる。利用者は1つの同意画面で、Graph の Mail.Read と自社 API の api.read の両方を許可できる |
| API ごと(Graph 用) | ⑤〜⑦ | /token で1回に頼めるスコープは、1つの API のものだけ。まず Graph の範囲でトークン1を取り、Graph を呼ぶ |
| API ごと(自社 API 用) | ⑧〜⑩ | 自社 API の範囲で、もう一度 /token を呼んでトークン2を取り、自社 API を呼ぶ |
⑧で2枚目のトークンを取るときは、①〜③をやり直す必要はありません。①で offline_access を頼んでおくとリフレッシュトークンが届くので、それを使ってトークンエンドポイントに頼めます。Microsoft のドキュメントも、リフレッシュトークンは同意済みのすべての許可に使え、たとえば mail.read の要求で受け取ったリフレッシュトークンで、別の API のトークンを頼めると説明しています。自社 API の範囲は③で同意済みなので、画面も出ません。MSAL を使う場合は、この2枚目の取得をライブラリが画面を出さずに行ってくれます。
トークンには、宛先の API(aud)と、その API で許された範囲(scp)が入ります。トークン1は宛先が Graph なので、自社の API に見せても受け付けてもらえません。複数の API を呼ぶアプリは、API ごとにトークンを取り分け、呼ぶ API に合ったトークンを付けて送ります。
API の側:スコープを見て判断する
スコープを最後に判断するのは、トークンを受け取る API(リソースサーバ)です。ここでは例として、アプリが Microsoft Graph からメールの一覧を読む場面で、流れを追ってみます。Graph を呼ぶ場合、図8の③〜⑤の確認は、Microsoft が Graph の中で行います。自社の API を作る場合は、同じ確認を自分の API に組み込みます。Microsoft のドキュメントも、API はトークンの中のスコープが自分の公開しているものか確かめ、その値で認可の判断をするよう書いています。
| 手順 | 何が起きるか |
|---|---|
| ①② | 利用者の操作をきっかけに、アプリがトークンを Authorization: Bearer ヘッダーに付けて Graph を呼ぶ |
| ③ | Graph が、トークンそのものが正しいかを確かめる(署名、発行元、宛先が Graph か、期限)。だめなら 401 |
| ④ | Graph が、この操作に要る範囲がトークンにあるかを確かめる。人の代わりなら scp、アプリ自身なら roles を見る。足りなければ 403 |
| ⑤ | 人の代わりに呼ぶときは、アプリに許された範囲に加えて、サインインしているその人の権限も見る。その人ができないことなら 403 |
| ⑥⑦ | 200 とメールの一覧は、アプリに返る。利用者が目にするのは、アプリがそれを画面に表示したもの |
200 で応じる相手は、利用者ではなくアプリです。401 や 403 も同じようにアプリに返るので、アプリはそれぞれの場合の動きを用意しておきます。たとえば、401 ならトークンを取り直し、403 なら足りない範囲の同意を頼むか、その機能が使えないことを画面に出します。
| クレーム | 入る場面 | 中身 |
|---|---|---|
| scp | 人の代わりに呼ぶとき(委任) | アプリが頼み、同意を得たスコープ。スペース区切りの文字列。アプリ自身として呼ぶトークンには入らない |
| roles | アプリ自身として呼ぶとき(クライアントクレデンシャル) | アプリに与えられたアプリケーションのアクセス許可(アプリロール)の一覧 |
範囲が足りないときの返し方も、RFC 6750 で決まっています。
| エラー | 状態 | 意味(RFC 6750) |
|---|---|---|
| invalid_token | 401 | トークンの期限切れ、取り消し、形が正しくないなど |
| insufficient_scope | 403 | トークンの範囲より高い権限が必要。必要なスコープを応答に入れてもよい |
「トークンそのものがだめ」なら 401、「トークンは正しいが範囲が足りない」なら 403、と分かれています。トラブルのときに、どちらが返っているかを見ると切り分けやすくなります。Graph の場合、エラーの詳しい理由は、応答の本文(JSON)にも入って返ってきます。また、図8の⑤のとおり、委任ではスコープがあっても、その人自身ができないことはできません。スコープは「アプリに任せた範囲」で、「その人の権限」を超えるものではないからです。
スコープは最小限に
OAuth 2.0 のセキュリティのベストプラクティス(RFC 9700)は、アクセストークンの権限を、そのアプリや使い方に必要な最小限に絞るよう求めています。Microsoft も、開発者は最小権限の原則を守り、アプリが動くのに必要な許可だけを頼むよう書いています。
必要になったときに少しずつ頼む(段階的な同意)
Microsoft では、すべてのスコープを最初にまとめて頼む必要はありません。最初は最低限のスコープだけを頼み、利用者が機能を使い始めたときに、必要なスコープを追加で頼めます。
新しく頼んだスコープは、それまでに許されたスコープに足されます。トークンには、それまでにアプリに許されたスコープがまとめて入ります。Microsoft は、最初からたくさんの許可を頼むと利用者や管理者が許可をためらいやすいので、基本の機能に要るものから頼み、機能に合わせて足していくのがよいとしています。なお、段階的な同意が使えるのは、委任のアクセス許可だけです。
.default:アプリ登録に書いた許可をまとめて頼む
https://graph.microsoft.com/.default のように、API の識別子に /.default を付けると、アプリ登録にあらかじめ書いておいた許可をまとめて頼む意味になります。
| 場面 | .default の扱い |
|---|---|
| クライアントクレデンシャル | 必ず .default を使う。アプリケーションのアクセス許可は、1つずつ指定して頼めない |
| 人の代わり(委任) | まだ同意がなければ、アプリ登録に書いた許可の同意画面が出る。同意済みなら、許された範囲がまとめてトークンに入る |
| 組み合わせ | .default と個別のスコープを1回の要求で混ぜることはできない(エラーになる) |
Microsoft は、管理者の同意が要る許可は、アプリ登録の画面に一覧として書いておくよう勧めています。そうすれば、管理者が組織全員の分をまとめて同意しやすくなります。
注意点
| 注意点 | 理由と対策 |
|---|---|
| 念のため広く頼まない | 範囲が広いほど、トークンが漏れたときの影響が大きい。.All の付く許可は、本当に全体が必要なときだけにする |
| 判断は API でする | アプリの画面でボタンを隠しても、トークンさえあれば API は呼べる。範囲の確認は、API が scp や roles を見て行う |
| 応答の scope を確かめる | 頼んだ範囲がそのまま許されるとは限らない。足りない機能は使えないことを、アプリが利用者に伝えられるようにする |
| 401 と 403 を見分ける | 401 はトークンそのものの問題、403(insufficient_scope)は範囲の不足。対処が違う |
企業での例
| 場面 | 頼むスコープの例 | 押さえどころ |
|---|---|---|
| 社内ポータルで、本人の予定だけを表示する | User.Read、Calendars.Read(委任) | 本人の予定を読むだけなので、書き込みや全員分の許可は頼まない |
| 夜間に全社員のプロフィールを人事システムと突き合わせる | https://graph.microsoft.com/.default(アプリケーション。中身は User.Read.All など) | 管理者の同意が必要。テナント全体に届くので、読む許可だけにとどめる |
| 自社の API を「読む」と「書く」で分けて公開する | api://<ID>/orders.read、api://<ID>/orders.write | API の登録でスコープを分けておき、API のコードで scp を見て操作ごとに判断する |
まとめ
| 項目 | 内容 |
|---|---|
| スコープとは | アクセストークンで「何をしてよいか」の範囲。開けてよい扉の一覧 |
| 書き方 | RFC:スペース区切り、大文字小文字を区別、意味は認可サーバが決める。名前と書き方は API の持ち主が決め、アプリはドキュメントで調べて実装する。Microsoft:リソースの識別子+許可の名前(API によって違いあり) |
| 決まり方 | アプリが頼み、利用者や管理者が同意し、認可サーバが最終的に決める。結果は応答の scope で返る |
| トークン | API ごとに1枚。中には scp(委任)や roles(アプリ自身)として入る |
| API の判断 | トークンが正しくなければ 401、範囲が足りなければ 403(insufficient_scope) |
| 頼み方 | 最小限に。必要になったときに少しずつ頼む。クライアントクレデンシャルは .default |
参考にした資料
この記事は、次の標準と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。
- RFC 6749: The OAuth 2.0 Authorization Framework(IETF):スコープの書き方(スペース区切り、大文字小文字の区別、意味は認可サーバが決める)、全部または一部を無視できること、違うときは応答の scope で知らせること、省いたときの扱い
- RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage(IETF):invalid_token(401)と insufficient_scope(403)、scope の値は利用者に見せるためのものではないこと
- RFC 9700: Best Current Practice for OAuth 2.0 Security(IETF):アクセストークンの権限を必要最小限に絞ること
- Scopes and permissions in the Microsoft identity platform・Developer’s guide to requesting permissions and consent in Microsoft Identity Platform(Microsoft Learn):最小権限、リソースの識別子+許可の名前、Graph を省く書き方、管理者しか同意できない許可、OpenID Connect のスコープ(address と phone は非対応)、.default、段階的な同意
- Access tokens in the Microsoft identity platform・Access token claims reference・Secure applications and APIs by validating claims(Microsoft Learn):トークンの中の1つ1つの情報をクレームと呼ぶこと、scp と roles の中身(scp はスペース区切りで委任のトークンに入る、roles はクライアントクレデンシャルで使う)、アプリ自身のトークンに scp が入らないこと、API での確かめ方
- Acquire and cache tokens using the Microsoft Authentication Library (MSAL)・Resources and scopes・Authorize applications, resources, and workloads with Microsoft Entra ID(Microsoft Learn):自社 API のスコープの書き方、同意済みのスコープなら後から画面なしでトークンを取れること、スコープの書き方は API と受け付ける aud で変わること(Graph・Azure Resource Manager・スキームを付けない API の例)、トークンは API ごとであること、許されたスコープがまとめてトークンに入ること、管理者の同意の要否、少しずつ頼むのがよいこと
- Microsoft Graph error responses and resource types・Resolve Microsoft Graph authorization errors(Microsoft Learn):401 と 403 の意味、エラーが HTTP の状態コードと JSON の本文で返ること、委任ではアプリに許された範囲とサインインしている人の権限の両方で判断すること
- Configure an application to expose a web API・Microsoft identity platform and OAuth 2.0 authorization code flow(Microsoft Learn):スコープの名前の付け方の慣例、/authorize と /token でのスコープの扱い、リフレッシュトークンで別の API のトークンを頼めること、リクエストと応答の例
あとがき
スコープは文字列の一覧にすぎないのですが、追いかけてみると、頼む・同意する・絞られる・トークンに入る・API が見る、と何人もの手を渡っていました。特に「アプリの画面でボタンを隠しても守ったことにはならず、最後に判断するのは API」という点は、自社の API を作るときに忘れずにいたいところです。Entra ID のアプリ登録で .All の付いた許可を見かけたら、本当に全体が必要なのか、一度立ち止まって考えたいと思いました。
コメント