【図解】OAuth 2.0のクライアントクレデンシャルフローとは?人がいないバッチやサーバ間連携がトークンを取るしくみを、Entra IDの証明書とアプリケーションの許可で初心者向けに解説

OAuth 2.0 の記事からスコープの記事までは、人がサインインして、アプリに「任せる」場面を中心に見てきました。ただ、夜間のバッチや監視のしくみのように、人がいないところで動くアプリも API を呼びます。この記事では、そうしたアプリがアクセストークンを取るクライアントクレデンシャルフローを、RFC と Microsoft(Entra ID)のリクエスト例で確かめながら図にしました。

この記事でわかること
  1. クライアントクレデンシャルは、人がいないときに、アプリが自分自身としてトークンを取るしくみ
  2. アプリの証明は、シークレットより証明書。秘密鍵で署名し、公開鍵で検証する
  3. 鍵ペアを作るところから API を呼ぶまでの流れ(Entra ID の場合)
  4. リフレッシュトークンがない理由
  5. アプリケーションの許可はテナント全体に届くので、範囲を絞る

まずはたとえで:委任状と通行証

これまで見てきた認可コードフローは、利用者が「このアプリに任せます」と委任状を書くようなものでした。アプリは人の代わりに動き、できることはその人の権限の内側に収まります。

クライアントクレデンシャルは、管理者が前もってアプリに通行証を渡しておくようなものです。アプリは誰かの代わりではなく、アプリ自身として動きます。そのため、人の権限には縛られず、与えられた許可によってはテナント全体のデータに届きます。

人がいる:認可コードフロー利用者任せる委任状アプリ人の代わりに動く(委任)トークン:scp(任された範囲)使えるのは、その人ができることまで人がいない:クライアントクレデンシャル管理者前もって通行証アプリ(サーバ)アプリ自身として動くトークン:roles(与えられた許可)人の権限には縛られない。許可しだいでテナント全体に届く違いは「誰の名前で動くか」。人の代わりか、アプリ自身か
図1 委任状(人の代わり)と通行証(アプリ自身)

どんなときに使うか

RFC 6749 では、クライアントクレデンシャルのグラント(トークンのもらい方)を、アプリが自分の資格情報(クライアントクレデンシャル)だけでアクセストークンを頼む方式としています。人のサインインも同意の画面もありません。また、このグラントは機密クライアントだけが使うよう、決められています。機密クライアントとは、シークレットや鍵を安全に保管できるアプリのことで、自社のサーバで動くアプリが当てはまります。ブラウザだけで動く SPA や、利用者に配るスマホアプリは、秘密を安全に置けないので使えません。

API を呼ぶアプリ① 人がサインインして、その人のデータを使う?はい認可コードフローアプリは人の代わりに動くいいえ② アプリは、秘密を安全に置ける場所(自社のサーバなど)で動く?はいクライアントクレデンシャル夜間のバッチ・サーバ間の連携監視や同期のしくみいいえ使えない配るアプリ(SPA・スマホ)には秘密を置けない人がいない、かつ秘密を守れるサーバで動くときに使う
図2 どのグラントを使うか

Microsoft のドキュメントは、この方式を、利用者の操作なしに裏で動く、サーバどうしのやり取りに使うものとしています。こうしたアプリは「デーモン」(常駐して裏で動くプログラム)や「サービスアカウント」と呼ばれることもあります。一方で、利用者が自分のデータを扱う場面では、アプリ自身としてのアクセスを使ってはいけないとも書いています。アプリ自身としてのアクセスは、人の代わりのアクセスより広く強いからです。

基本の形(RFC):シークレットを送ってトークンをもらう

まず、いちばん単純な形を RFC 6749 の例で見ます。アプリはトークンエンドポイントに、grant_type=client_credentials だけを送っています。アプリの証明(クライアント ID とシークレット)は Authorization ヘッダーに入っています。

リクエスト(RFC 6749 の例)

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
リクエスト(アプリ → トークンエンドポイント)POST /tokenトークンエンドポイントへ。TLS が必須Authorization: Basic …アプリの証明。client_id:client_secret を Base64 にしたものgrant_type=client_credentialsこのグラントを使う合図scope=…(任意)頼む範囲。省いたときの扱いは認可サーバが決める応答(トークンエンドポイント → アプリ)access_tokenアクセストークンtoken_type ・ expires_inトークンの種類と、有効な秒数refresh_token入れない(RFC では SHOULD NOT)アプリの証明そのものが、トークンをもらう根拠になる
図3 リクエストと応答の中身(RFC 6749)

Basic の後ろの文字列は、クライアント ID とクライアントシークレットを : でつないで、Base64 という形式に変えたものです。Base64 は文字の置き換えにすぎず、暗号ではありません。実際に元に戻してみると、次のとおりです。

$ echo 'czZCaGRSa3F0MzpnWDFmQmF0M2JW' | base64 -d; echo
s6BhdRkqt3:gX1fBat3bV

だれでも元に戻せるので、この通信を守っているのは TLS です。RFC 6749 も、トークンエンドポイントには TLS を必須としています。なお RFC 6749 は、クライアント ID とシークレットをリクエストの本文に入れる方法も認めていますが、推奨はしておらず、ヘッダー(Basic)を使えないアプリ向けの方法としています。

応答(RFC 6749 の例)

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"2YotnFZFEjr1zCsicMWpAA",
  "token_type":"example",
  "expires_in":3600,
  "example_parameter":"example_value"
}

応答にはアクセストークンと有効な秒数が入ります。RFC 6749 は、このグラントの応答にはリフレッシュトークンを入れるべきではない(SHOULD NOT)としています。理由は後の「期限が切れたら」で見ます。

RFC 6749 の図では、やり取りはこの (A) 要求と (B) 応答の2つだけです。ただ、この形ではシークレットそのものが毎回ネットワークを流れます。そこで次に、本番で勧められている証明書の方式を見ます。

アプリの証明:シークレットより証明書

クライアントクレデンシャルでは、アプリの証明がそのままトークンをもらう根拠になります。そのため、アプリの証明をどう持つかがいちばん大事になります。

シークレットアプリが持つもの文字列送るもの文字列そのものEntra ID の確かめ方同じ文字列と照合ポイント漏れるとそのまま使われる証明書アプリが持つもの秘密鍵送るもの秘密鍵で署名した短い JWTEntra ID の確かめ方公開鍵で確かめるポイント秘密鍵は送らないフェデレーションアプリが持つもの長く使う秘密なし送るもの外部の発行元のトークンEntra ID の確かめ方信頼の設定と照合ポイント例:GitHub Actions、Kubernetes開発や試しに使う本番ではこちら(Microsoft の推奨)秘密そのものを送らない方式ほど、漏れたときの被害が小さい
図4 アプリの証明の3つのやり方

シークレットは、同じ文字列を Entra ID とアプリの両方が持ち、毎回そのまま送ります。漏れれば、だれでもそのアプリとしてトークンを取れてしまいます。証明書では、アプリは秘密鍵で署名した短い JWT(署名つきのトークンの形式)を送るだけで、秘密鍵そのものは送りません。RFC 9700 も、クライアントの認証には公開鍵の方式(mTLS や署名した JWT)を使うよう勧めています。認可サーバ側が共有の秘密を持たなくて済むので、鍵が漏れにくいからです。

この記事に出てくる2組の鍵

証明書の方式では、秘密鍵と公開鍵が出てきます。秘密鍵で署名したものは、対になる公開鍵でしか検証できません(くわしくは暗号の基礎の記事)。流れを見る前に、誰がどの鍵を持つのかを先に押さえておきます。鍵の組は2つあります。

アプリ(サーバ)Entra IDAPIアプリの鍵ペアアプリを証明するEntra ID の鍵ペアトークンを証明する秘密鍵ここで作り、外に出さない→ 署名に使う公開鍵証明書として登録してもらう→ 検証に使う(使わない)(使わない)トークンは読まずに運ぶ秘密鍵Entra ID だけが持つ→ トークンに署名公開鍵Entra ID が公開している→ トークンを検証公開鍵だけ渡す公開秘密鍵:署名する。持ち主だけが持つ公開鍵:検証する。配ってよい署名はいつも秘密鍵で、検証はいつも公開鍵で
図5 誰がどの鍵を持つか
  • アプリの鍵ペア:アプリを証明するための鍵です。秘密鍵はアプリのサーバで作って、そこから出しません。公開鍵だけを証明書にして、Entra ID に登録します。アプリが秘密鍵で署名し、Entra ID が公開鍵で検証します。
  • Entra ID の鍵ペア:トークンを証明するための鍵です。Entra ID が秘密鍵でアクセストークンに署名し、API は Entra ID が公開している公開鍵で検証します。アプリはこの鍵を使わず、トークンを読まずに運ぶだけです。
アプリの証明Microsoft のドキュメントでの扱い
証明書推奨される種類。Entra ID には証明書(公開鍵)だけを登録し、秘密鍵はアプリの手元に置く。試すときは自己署名の証明書でもよいが、本番では、よく知られた認証局が署名した証明書を使い、Azure Key Vault で管理するよう勧めている
シークレットアプリのパスワードにあたる文字列。証明書より安全性が低く、本番では証明書かフェデレーションを使うよう書かれている。期限は最長24か月で、12か月未満を推奨。値は作ったときに一度しか表示されない
フェデレーション外部の発行元(GitHub Actions や Kubernetes など)が出したトークンを、アプリの証明として使う。アプリは長く使う秘密を持たない
補足:秘密を持たない方法

アプリが Azure の中で動くなら、マネージド ID を使う方法もあります。Microsoft の MSAL のドキュメントは、Azure の中ならマネージド ID(保管も更新もする資格情報がない)、Azure の外(GitHub Actions やほかのクラウドなど)ならフェデレーション、どちらも使えないときに証明書、という順番を勧め、シークレットはローカルでの開発や試作だけにするよう書いています。

全体の流れ(Entra ID・証明書の場合)

図5の鍵が、どこで作られ、どこを通って、どこで使われるのかを、一歩ずつ並べます。流れは、最初に1回だけ行う「準備」と、トークンが要るたびに行う「実行時」に分かれます。番号は2つの図を通しての番号です。

準備(最初に1回):①〜⑦

準備で手を動かすのは管理者です。管理者は、自分の PC からアプリのサーバにログインしたり、ブラウザで Entra 管理センターを開いたりして作業します。

管理者(自分の PC)アプリ(サーバ)Entra ID(管理センター)① サーバにログインし、鍵ペアを作る例:New-SelfSignedCertificateサーバの中に鍵ペアができる秘密鍵:サーバの中に残す公開鍵:証明書に入っている② 公開鍵だけを .cer に書き出し、手元へ秘密鍵は入らない(サーバに残る)③ アプリを登録するクライアント ID とテナント ID が決まる④ .cer をアップロードするCertificates & secrets ›› Upload certificate公開鍵を保管する証明書の拇印が表示される⑤ アプリの設定に書くクライアント ID・テナント ID・拇印⑥ アプリケーションの許可を付けるAPI permissions⑦ 管理者が同意するGrant admin consent秘密鍵はサーバから動かない。動くのは公開鍵(証明書)と ID だけ
図6 準備の流れ:鍵ペアを作ってから、許可に同意するまで
手順どこで何をするか鍵はどうなるか
①サーバ管理者がアプリのサーバにログインし、コマンドで鍵ペア(と証明書)を作る秘密鍵と公開鍵がサーバの中にできる
②サーバ → 管理者の PC公開鍵だけを証明書ファイル(.cer)に書き出し、管理者の PC にコピーする公開鍵だけが動く。秘密鍵はサーバに残る
③Entra 管理センターアプリを登録する。クライアント ID とテナント ID が決まる―
④Entra 管理センター.cer をアップロードする。画面に証明書の拇印が表示される公開鍵が Entra ID に届く
⑤サーバクライアント ID・テナント ID・証明書の拇印を、アプリの設定に書くアプリが、どの秘密鍵を使うか分かる
⑥Entra 管理センターアプリケーションの許可を付ける―
⑦Entra 管理センター管理者が同意する―

②で動かすのは公開鍵だけです。公開鍵は配ってよい鍵なので、管理者の PC を経由しても困りません。気を付けるのは、別の証明書と取り違えないことくらいです。

補足:鍵ペアを別の場所で作る場合

Microsoft のドキュメントには、鍵ペアを管理者の PC で作る手順も載っています。この場合、公開鍵は同じように .cer にしてアップロードします。秘密鍵は、パスワードで保護した .pfx ファイルに書き出して、アプリが動くサーバに置きます。秘密鍵が一度だけ、パスワード付きで移動する形です。本番では、認証局が署名した証明書を Azure Key Vault で管理するよう勧められています。

実行時(トークンが要るたび):⑧〜⑳

実行時に人は出てきません。アプリ(サーバ)、Entra ID、API の3者だけでやり取りします。

アプリ(サーバ)Entra ID(トークン EP)API(例:Graph)トークンをもらう(⑧〜⑮)API を呼ぶ(⑯〜⑳)⑧ JWT を作る宛先・クライアント ID・期限を書く⑨ 秘密鍵で JWT に署名する⑩ POST /tokenclient_credentials + 署名した JWT + .default⑪ どの証明書かを探すJWT のヘッダーにある拇印で⑫ 登録した公開鍵で署名を検証本物のアプリが作り、書き換えられていない⑬ 宛先・発行者・期限を確かめるaud・iss・exp など⑭ トークンを作って署名するroles を入れ、Entra ID の秘密鍵で⑮ アクセストークンリフレッシュトークンはない⑯ Bearer トークンで呼ぶ⑰ 公開鍵を取りに行くjwks_uri(取得済みなら省略)⑱ Entra ID の公開鍵⑲ 公開鍵で署名を検証roles を見て判断する⑳ 200 + データ期限が切れたら ⑧ からもう一度アプリは自分の秘密鍵で署名し、Entra ID はトークンに自分の秘密鍵で署名する
図7 実行時の流れ:JWT に署名してから、API の応答を受け取るまで
手順誰が何をするか使う鍵
⑧アプリ宛先・クライアント ID・期限を書いた JWT を作る―
⑨アプリJWT に署名するアプリの秘密鍵で署名
⑩アプリ署名した JWT を付けて、トークンエンドポイントに頼む(RFC の A)―
⑪Entra IDJWT のヘッダーにある拇印で、登録されたどの証明書かを探す―
⑫Entra IDJWT の署名を検証する。本物のアプリが作り、途中で書き換えられていないことが分かるアプリの公開鍵で検証
⑬Entra ID宛先・発行者・期限を確かめる―
⑭Entra ID許可を roles に入れたアクセストークンを作り、署名するEntra ID の秘密鍵で署名
⑮Entra IDアクセストークンを返す(RFC の B)。リフレッシュトークンは返さない―
⑯アプリトークンを付けて API を呼ぶ―
⑰⑱APIEntra ID が公開している公開鍵を取りに行く。取得済みなら、それを使うEntra ID の公開鍵を受け取る
⑲APIトークンの署名を検証し、roles を見て判断するEntra ID の公開鍵で検証
⑳API応答(200 とデータ)を返す―

署名と検証が2回ずつ出てきます。⑨と⑫は「アプリの鍵ペア」で、アプリが本物かを確かめるためのものです。⑭と⑲は「Entra ID の鍵ペア」で、トークンが本物かを確かめるためのものです。どちらも、署名は秘密鍵で、検証は公開鍵で行います。

シークレットの場合:❶〜⓭

シークレットを使う場合も、同じように一歩ずつ並べます。証明書の流れと区別するため、番号は黒丸(❶❷…)にしています。

管理者アプリ(サーバ)Entra IDAPI(例:Graph)準備(最初に1回)実行時(トークンが要るたび)❶ アプリを登録するクライアント ID とテナント ID が決まる❷ シークレットを作るCertificates & secrets ›› New client secretEntra ID が値を作る照合のための情報が Entra ID 側に残る❸ シークレットの値画面に一度だけ表示される❹ アプリの設定に書く値・クライアント ID・テナント ID❺ アプリケーションの許可を付ける❻ 管理者が同意する❼ POST /token(A)client_credentials + シークレットの値そのもの + .default❽ 登録した値と同じかを照合❾ トークンを作って署名するroles を入れ、Entra ID の秘密鍵で❿ アクセストークン(B)リフレッシュトークンはない⓫ Bearer トークンで呼ぶ⓬ 公開鍵で署名を検証roles を見て判断する⓭ 200 + データ証明書との違い:秘密の値そのものがサーバへ運ばれ、要求のたびに送られる
図8 シークレットの場合の流れ(準備と実行時)
手順誰が・どこで何をするか証明書の場合(図6・図7)との違い
❶管理センターアプリを登録する。クライアント ID とテナント ID が決まる③と同じ
❷管理センターCertificates & secrets ›› New client secret でシークレットを作る。値を作るのは Entra ID①②④の代わり。サーバで鍵を作らない
❸管理センター → 管理者シークレットの値が、画面に一度だけ表示される秘密が管理者の手元を通る
❹サーバ値・クライアント ID・テナント ID をアプリの設定に書く⑤と同じ。ただし置くのは秘密の値そのもの
❺❻管理センターアプリケーションの許可を付け、管理者が同意する⑥⑦と同じ
❼アプリシークレットの値そのものを付けて、トークンエンドポイントに頼む(RFC の A)⑧⑨(署名)がない。毎回、秘密そのものを送る
❽Entra ID登録した値と同じかを照合する⑪⑫(公開鍵での検証)の代わり
❾〜⓭Entra ID・APIトークンを作って署名し、返す。API は署名を検証し、roles を見て応じる⑭〜⑳と同じ

流れの後半(❾〜⓭)は、証明書の場合とまったく同じです。違いは前半にあります。シークレットでは、秘密の値が Entra ID で作られて、管理者の手元を通ってサーバに運ばれ(❷〜❹)、トークンを頼むたびにネットワークを流れます(❼)。通信は TLS で守られていますが、どこかで値が漏れれば、だれでもそのアプリとしてトークンを取れてしまいます。証明書なら、秘密鍵はサーバから動かず、ネットワークに出るのは期限の短い署名だけです。Microsoft が本番では証明書を勧めているのは、この違いがあるからです。

Microsoft(Entra ID)での手順

ここからは Microsoft の場合です。図6・図7の番号に沿って、実際の操作とリクエストを見ます。

鍵ペアを作り、証明書を登録する(図6の①〜⑤)

①では、アプリのサーバで鍵ペアを作ります。Microsoft のドキュメントでは、試すための自己署名の証明書を、Windows の PowerShell で作る例が載っています。New-SelfSignedCertificate を実行すると、鍵ペアと証明書が、そのコンピュータの証明書ストア(実行したユーザーの個人用ストア)にできます。

① 鍵ペアと証明書を作る(Microsoft のドキュメントの例)

$certname = "{certificateName}"    ## Replace {certificateName}
$cert = New-SelfSignedCertificate -Subject "CN=$certname" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256

②では、公開鍵だけを .cer ファイルに書き出します。Export-Certificate で書き出したファイルには、秘密鍵は入りません。このファイルを管理者の PC にコピーします。

② 公開鍵だけを .cer に書き出す(Microsoft のドキュメントの例)

Export-Certificate -Cert $cert -FilePath "C:\Users\admin\Desktop\$certname.cer"   ## Specify your preferred location

③④では、Entra 管理センターでアプリを登録し、Entra ID ›› App registrations ›› (アプリ) ›› Certificates & secrets ›› Certificates ›› Upload certificate から .cer をアップロードします。アップロードできるのは .cer・.pem・.crt のファイルです。アップロードすると、拇印・開始日・有効期限が表示されます。⑤では、クライアント ID・テナント ID と、この拇印をアプリの設定に書きます。

アプリの登録(Entra ID)アプリケーション(クライアント)ID→ リクエストの client_idディレクトリ(テナント)ID→ URL の {tenant}証明書とシークレット(どれか)証明書公開鍵だけを登録する(推奨)シークレット文字列。期限は最長24か月フェデレーション外部の発行元を信頼するアプリ(サーバ)手元に置くもの(登録と対)証明書なら:秘密鍵シークレットなら:同じ文字列Entra ID には公開鍵を、サーバには秘密鍵を置く(証明書の場合)
図9 STEP 1 のあと:Entra ID にあるものと、サーバにあるもの

なお、自己署名の証明書はテスト用です。本番では、よく知られた認証局が署名した証明書を使うよう勧められています。シークレットを使う場合は、同じ画面の Client secrets ›› New client secret で作ります。作ったときに表示される値をアプリの設定に書きます。値はその画面を離れると二度と表示されません。

アプリケーションの許可を付け、管理者が同意する(図6の⑥⑦)

Microsoft のアクセス許可には、人の代わりに使う「委任されたアクセス許可」と、アプリ自身として使う「アプリケーションの許可」があります。人がいないクライアントクレデンシャルでは、代わりになる人がいないので、委任されたアクセス許可は使えません。使えるのはアプリケーションの許可だけです。アプリケーションの許可は「アプリロール」とも呼ばれ、トークンでは roles クレームに入ります(スコープの記事の scp と roles を参照)。

委任されたアクセス許可アプリケーションの許可誰として人の代わりアプリ自身トークンscp に入るroles に入る同意する人利用者か管理者管理者だけ頼み方個別に・少しずつも可.default でまとめて① 許可を足すAPI permissions でApplication を選ぶ② 管理者が同意Graph の許可は特権ロール管理者以上③ roles に入る次に取るトークンに許可が入るクライアントクレデンシャルで使えるのは、アプリケーションの許可だけ
図10 委任されたアクセス許可とアプリケーションの許可

⑥では、API permissions ›› Add a permission ›› Microsoft Graph ›› Application permissions で許可を選びます。⑦では、Grant admin consent で管理者が同意します。アプリケーションの許可は、管理者が同意するまで使えません。Microsoft Graph のアプリケーションの許可に同意できるのは、少なくとも特権ロール管理者のロールを持つ人です。アプリケーション管理者やクラウドアプリケーション管理者は、許可を足すことはできても、Graph の許可に同意することはできません。許可を足す人と同意する人を分けておけば、ひとりの判断で強い許可が付くのを防げます。

また、アプリケーションの許可は、段階的な同意のように実行中に少しずつ頼むことができません。必要な許可は、アプリの登録にあらかじめすべて書いておきます。

JWT に署名して、トークンを取る(図7の⑧〜⑮)

⑧⑨でアプリが作って署名する JWT は、次のような中身です。

ヘッダーalgPS256(署名のしかた)typJWTx5t#S256証明書の拇印(どの公開鍵で検証するか)ペイロードaudトークンエンドポイントの URL(宛先)iss ・ subアプリのクライアント IDjtiこの JWT だけの IDnbf ・ exp使える期間(長くても5〜10分)署名ヘッダーとペイロードに、アプリの秘密鍵で署名した値送るときは、3つを Base64URL にして「.」でつなぐヘッダー.ペイロード.署名中身はだれでも読めるが、署名があるので書き換えるとばれる
図11 アプリが作る JWT(client_assertion)の中身

Microsoft のドキュメントでは、署名のアルゴリズムは PS256 にするよう書かれています。また、exp は nbf から長くても5〜10分にするよう勧めています。この形式は OpenID Connect の private_key_jwt というクライアント認証の方式にあたります。実際には、MSAL(Microsoft の認証ライブラリ)などのライブラリが、この JWT を作って署名してくれます。

⑩では、署名した JWT を client_assertion に入れて、トークンエンドポイントに送ります。Microsoft のドキュメントに載っているリクエスト例です。値はドキュメントの例のものです。

⑩ リクエスト(Microsoft のドキュメントの例:証明書)

POST /{tenant}/oauth2/v2.0/token HTTP/1.1               // Line breaks for clarity
Host: login.microsoftonline.com:443
Content-Type: application/x-www-form-urlencoded

scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_id=11112222-bbbb-3333-cccc-4444dddd5555
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJhbGciOiJSUzI1NiIsIng1dCI6Imd4OHRHeXN5amNScUtqRlBuZDdSRnd2d1pJMCJ9.eyJ{a lot of characters here}M8U3bSUKKJDEg
&grant_type=client_credentials

scope には、使いたい API の識別子に /.default を付けたものを入れます。Graph なら https://graph.microsoft.com/.default で、「この API について、アプリに付いているアプリケーションの許可をまとめて」という意味です。1回の要求に入れられるのは1つの API の分だけで、複数の API を入れるとエラーになります。

⑪〜⑭で Entra ID が確かめてトークンを作り、⑮で返します。

⑮ 応答(Microsoft のドキュメントの例)

{
  "token_type": "Bearer",
  "expires_in": 3599,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1uQ19WWmNBVGZNNXBP..."
}

応答には、リフレッシュトークンは入っていません。Microsoft のドキュメントも、このフローでリフレッシュトークンが出ることはないと書いています。

シークレットを使う場合は、client_assertion_type と client_assertion の代わりに client_secret を送ります。違いはこの部分だけです。シークレットは、この例のように本文で送るほか、RFC と同じ Basic のヘッダーで送ることもできます。

リクエスト(Microsoft のドキュメントの例:シークレット)

POST /{tenant}/oauth2/v2.0/token HTTP/1.1           //Line breaks for clarity
Host: login.microsoftonline.com:443
Content-Type: application/x-www-form-urlencoded

client_id=00001111-aaaa-2222-bbbb-3333cccc4444
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_secret=A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u
&grant_type=client_credentials

トークンで API を呼ぶ(図7の⑯〜⑳)

⑯では、受け取ったアクセストークンを Authorization: Bearer ヘッダーに付けて、API を呼びます。次の例は、Graph でユーザーの一覧を読むリクエストです。

⑯ リクエスト(Microsoft のドキュメントの例)

GET /v1.0/users HTTP/1.1
Host: graph.microsoft.com:443
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbG...

⑰⑱では、API が Entra ID の公開鍵を手に入れます。Entra ID は、トークンの署名に使う鍵の公開鍵を、OpenID Connect のメタデータにある jwks_uri の場所で公開しています(OAuth 2.0 の記事のメタデータも参照)。鍵は定期的に入れ替わるので、Microsoft は24時間ごとくらいに取り直すのが妥当としています。毎回取りに行くわけではありません。⑲では、トークンのヘッダーの kid を見て、どの公開鍵で検証するかを選び、署名を確かめたうえで中身を見て判断します。このトークンには、人の代わりのトークンとは違う情報が入ります。

アプリ自身のトークンaud宛先の APIrolesUser.Read.All などazpアプリ(クライアント)IDazpacr1=シークレット、2=証明書idtypapp(設定すると入る)人の代わりのトークン(比較)aud宛先の APIscpUser.Read などazpアプリ(クライアント)IDazpacrアプリの証明のしかたidtyp入らないアプリ自身のトークンには scp が入らないAPI は roles を見て判断する。azp・azpacr は v2.0 のトークンの名前(v1.0 では appid・appidacr)roles があるか、scp があるかで「誰として来たか」が分かる
図12 アプリ自身のトークンと、人の代わりのトークン(イメージ)
クレーム中身
rolesアプリに与えられたアプリケーションの許可。API はこれを見て判断する。アプリ自身のトークンには scp は入らない
azpトークンを使っているアプリのアプリケーション(クライアント)ID。v1.0 のトークンでは appid
azpacrアプリの証明のしかた。1 はクライアント ID とシークレット、2 は証明書。v1.0 のトークンでは appidacr
idtyp設定すると入る任意のクレーム。アプリ自身のトークンでは app になる。Microsoft は、アプリのトークンか人の代わりのトークンかを見分けるいちばん確かな方法としている

図12の値は説明のための例です。Microsoft のドキュメントは、自分のものではない API(Graph など)のトークンを、アプリのコードで読んだり確かめたりしないよう注意しています。トークンの中身を読んで確かめるのは、受け取る API の役目です。

アプリケーションの許可は、テナント全体に届く

人の代わりのアクセスでは、アプリができることは、その人ができることの内側に収まります。アプリ自身としてのアクセスにはその枠がありません。Microsoft のドキュメントは、アプリにアプリロールが割り当てられると、そのロールが対象にするすべてのリソースと操作に、制限なくアクセスできると説明しています。

委任の Mail.Read利用者 AABCDEFA のメールボックスだけアプリケーションの Mail.ReadそのままだとABCDEFテナントのすべてExchange Online の RBAC for Applications で絞るとABCDEF決めた範囲だけ人の権限に縛られないぶん、範囲はテナント全体になる。必要なら絞る
図13 委任の Mail.Read と、アプリケーションの Mail.Read が届く範囲

たとえば、Graph のアプリケーションの許可 Mail.Read は、すべてのメールボックスのメールを読めます。Mail.Send は、どのユーザーとしてもメールを送れます。Exchange Online では、メール・予定表・連絡先・メールボックスの設定についてのアプリケーションの許可は、既定で組織のすべてのメールボックスに届きます。

これを絞るしくみとして、Exchange Online には RBAC for Applications があります。管理スコープや管理単位で、アプリが触れるメールボックスを決めておけるしくみです。以前の Application Access Policies(アプリケーションのアクセスポリシー)は、これに置き換えられています。また、Microsoft は、アプリには本当に必要な許可だけを頼むよう求めており、たとえばユーザーを見分けるだけなら User.Read.All ではなく、範囲の狭い User.ReadBasic.All を使う例を挙げています。

期限が切れたら:リフレッシュトークンはない

アクセストークンには期限があります(例の応答では expires_in が 3599 秒)。期限が切れたら、アプリは STEP 3(図7の⑧)からもう一度、同じ要求を送ります。

クライアントクレデンシャル時間トークン1(約1時間)期限内は同じトークンを使い回す期限切れ同じ要求をもう一度トークン2アプリの証明で取り直す比べると:認可コードフローアクセストークンリフレッシュトークン次のトークン人がいなくても取り直すために使ういつでもアプリの証明で取り直せるので、リフレッシュトークンは要らない
図14 期限が切れたときの取り直し方

認可コードフローでリフレッシュトークンを使うのは、人にもう一度サインインしてもらわずにトークンを取り直すためでした。クライアントクレデンシャルでは、アプリはいつでも自分の証明でトークンを取り直せるので、リフレッシュトークンは要りません。Microsoft のドキュメントも、リフレッシュトークンを使うときにも必要なクライアント ID とシークレットがあれば、それだけでアクセストークンを取れるからだと説明しています。

ただし、呼び出しのたびに新しいトークンを頼むのは避けます。MSAL(Microsoft の認証ライブラリ)は、アプリ用のトークンをキャッシュし、期限内は同じトークンを返します。Microsoft のドキュメントは、トークンを頼む回数が多すぎると Entra ID 側で抑えられることがあるので、キャッシュを使うよう書いています。

うまくいかないとき

Microsoft のドキュメントに載っているエラーのうち、クライアントクレデンシャルでよく出会いそうなものです。

エラー意味確かめること
AADSTS7000215シークレットが正しくないシークレットの「値」を使っているか(シークレットの ID ではないか)。別のアプリの登録のシークレットではないか
AADSTS7000222シークレットの期限が切れているCertificates & secrets で期限を見て、新しいシークレットを作る。できれば証明書に切り替える
AADSTS700024証明書で作った JWT が、使える期間の外にある証明書の期限が切れていないか。サーバの時刻がずれていないか
AADSTS700016そのテナントにアプリが見つからないクライアント ID と、URL のテナントが合っているか
AADSTS70011
(invalid_scope)
scope の値が正しくないAPI の識別子に /.default を付けているか
AADSTS700022scope に複数の API が入っているAPI ごとに要求を分ける
API から 403アプリに必要な許可がないアプリケーションの許可を付けて、管理者が同意したか。委任されたアクセス許可を付けていないか

企業での例

場面許可とアプリの証明押さえどころ
夜間に、全社員のプロフィールを人事システムと突き合わせるGraph の User.Read.All(アプリケーション)。証明書見分けるだけなら User.ReadBasic.All で足りないかを先に考える
通知用のメールボックスから、自動でメールを送るバッチGraph の Mail.Send(アプリケーション)。証明書そのままだと、どのユーザーとしても送れる。RBAC for Applications で、通知用のメールボックスだけに絞る
GitHub Actions から、Azure の API を呼んでデプロイするフェデレーションGitHub にシークレットを置かずに済む
社内のシステムどうしで、自社の API を呼び合う呼ばれる側の API にアプリロールを作り、呼ぶ側のアプリに割り当てる呼ばれる側の API は、トークンの roles を確かめる

まとめ

項目内容
どんなもの人がいないときに、アプリが自分の証明だけでアクセストークンを取るグラント。アプリは人の代わりではなく、アプリ自身として動く
使える場面秘密を安全に置けるサーバで動くアプリ(機密クライアント)だけ。SPA や配るアプリでは使えない
流れ(A) アプリが証明を付けて頼む → (B) トークンが返る。リフレッシュトークンはなく、期限が切れたら同じ要求をもう一度送る
Entra ID の準備アプリを登録し、アプリの証明を登録し、アプリケーションの許可を付けて管理者が同意する。scope は /.default
トークンroles にアプリケーションの許可が入る。scp は入らない
アプリの証明シークレットは開発用。本番は証明書かフェデレーション(Azure の中ならマネージド ID)。証明書では、アプリの秘密鍵で署名し、Entra ID が登録された公開鍵で検証する
2組の鍵アプリの鍵ペア(アプリを証明する)と、Entra ID の鍵ペア(トークンを証明する)。署名はいつも秘密鍵で、検証はいつも公開鍵で
範囲人の権限に縛られず、テナント全体に届く。必要な許可だけにし、Exchange Online なら RBAC for Applications で絞る

参考にした資料

この記事は、次の標準と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。

あとがき

クライアントクレデンシャルは、流れだけを見ると往復が1回で、OAuth の中でいちばん簡単に見えます。ただ、ドキュメントを読み進めると、気を付けるところは流れではなく「アプリの証明をどう守るか」と「許可がどこまで届くか」にありました。特に、アプリケーションの Mail.Read が既定ですべてのメールボックスに届くことは、許可を付ける前に必ず思い出したいと思いました。

コメント