OAuth 2.0 の記事で、サインイン(認証)は OpenID Connect が足すもの、と書きました。その後、「ID トークンがなぜ必要なのか」を説明した解説動画の書き起こしを読んで、その説明が正しいかを RFC と仕様で調べ直したことがありました。この記事では、そのときに整理したことも含めて、OpenID Connect でサインインするしくみを、仕様と Microsoft(Entra ID)のドキュメントで確かめながら、最初の準備から一歩ずつ図にしました。
- OpenID Connect は、OAuth 2.0 の上に「誰がサインインしたか」を足すしくみ
- アクセストークンだけでサインインにしてはいけない理由と、ID トークンの役目
- ID トークンに入るものと、アプリが確かめること(aud・nonce など)
- Entra ID で、Web アプリにサインインするまでの流れと設定
- サインアウトは、アプリと Entra ID の両方のセッションを終える
まずはたとえで:名札と部屋の鍵
会議に参加するとき、受付で身分証を見せると、名札と部屋の鍵を渡されるとします。名札には、この会議用で、今日だけ有効で、受付の印が押してあります。会議の主催者は名札を見て、「この人は誰か」が分かります。一方、部屋の鍵は、部屋に入るためのものです。鍵を確かめるのは部屋の側で、鍵には誰の鍵かは書いてありません。
OAuth 2.0 が出すアクセストークンは、この部屋の鍵です。OpenID Connect(OIDC)は、そこに名札にあたる ID トークンを足します。Microsoft のドキュメントも、ID トークンを会議の名札にたとえて、「利用者が本人であることをアプリが確かめるための情報」と説明しています。
OAuth 2.0 との違い
OpenID Connect Core 1.0 の冒頭には、OpenID Connect は「OAuth 2.0 プロトコルの上に乗せた、シンプルな ID の層」と書かれています。OAuth 2.0 が答えるのは「このアプリに何をさせてよいか」(認可)で、OpenID Connect が答えるのは「誰がサインインしたか」(認証)です。
流れそのものは、認可コードフローとほとんど同じです。違いは、要求の scope に openid を入れることと、そうすると応答に ID トークンが入ることです。ID トークンは JWT で、アプリ(RP)あてに発行され、OP(認可サーバ)の秘密鍵で署名されています。
アクセストークンだけでサインインにしてはいけない理由
OAuth 2.0 だけでも、アクセストークンが取れたら「サインインできた」とみなすアプリを作れてしまいます。しかし、アクセストークンはサインインの根拠になりません。
- 誰あてか分からない:アクセストークンは API あてで、アプリは中身を読まない前提です(JWT の検証の記事)。そのトークンが自分のアプリのために出されたものか、アプリには判断できません。
- 今サインインしたとは限らない:OAuth の解説(oauth.net)は、アクセストークンは利用者がいなくなった後も長く使えるもので、持っているだけではアプリに何も伝えない、と説明しています。アクセストークンが示すのは「いつか、誰かがこのアプリに API を使うことを任せた」ことまでで、「今この人がアプリの前にいて、サインインした」ことではありません。たとえば次のような場面です。
- 月曜の朝に田中さんが一度サインインし、その後はブラウザを閉じて休暇に入ったとします。アプリはリフレッシュトークンで、田中さんがいない間もアクセストークンを取り直せます(Entra ID の既定では、使い続ければ90日)。水曜の夜中にアプリが持っている新しいアクセストークンは、田中さんがその時間にサインインしたことを示しません。
- クライアントクレデンシャルのように、そもそも人がいなくてもアクセストークンは出ます。「アクセストークンがある=誰かがサインインした」とは言えません。
iat、頼めばauth_time)が入っていて、nonceで「このサインインの要求への答え」だと分かります。 - 差し込まれても見分けられない:RFC 9700(OAuth 2.0 のセキュリティのベストプラクティス)は、盗まれたアクセストークンを正しいアプリに差し込み、ほかの人になりすます攻撃を挙げています。トークンが URL で返るインプリシットフローを例にしていて、OAuth だけではこれを見分ける方法がない、としています。そのうえで、OpenID Connect の ID トークン(中のアクセストークンのハッシュと
nonce)で防げること、インプリシットフローを使わず認可コードフローを使うことを勧めています。
ID トークンなら、aud に自分のクライアント ID が入っているか、nonce が自分の送った値と同じかを確かめられます。ほかのアプリあてに出た ID トークンや、自分の要求とは関係のない ID トークンは、ここで弾けます。サインインの根拠には、アプリあてに署名された ID トークンを使う、というのが OpenID Connect の考え方です。
基本の形(OpenID Connect Core)
登場人物と窓口
OpenID Connect では、ID トークンを出す側を OP(OpenID Provider)、ID トークンを受け取って信頼する側のアプリを RP(Relying Party)と呼びます。Entra ID が OP、サインインさせたい Web アプリが RP です。
OP の窓口の場所は、設定(Discovery)の文書にまとめて公開されています。アプリは、この文書から認可やトークンの窓口、公開鍵の場所(jwks_uri)を知ります。
認可コードフローに openid を足す
OpenID Connect Core の認可コードフローを、図にしました。アルファベットは、このあとの Entra ID の流れ(番号付き)と区別するためのものです。
| パラメータ | 意味(OpenID Connect Core の 3.1.2.1) |
|---|---|
| scope | 必須。openid を必ず入れる。入っていないと OpenID Connect の要求にならない |
| response_type | 必須。認可コードフローでは code |
| client_id | 必須。アプリのクライアント ID |
| redirect_uri | 必須。OP に事前に登録した値と完全に一致させる |
| state | 推奨。要求と応答を結ぶ値。CSRF(なりすましの要求)対策によく使う |
| nonce | 任意(認可コードフロー)。アプリのセッションと ID トークンを結ぶ値。再利用の攻撃を防ぐ |
(F) で ID トークンが返るのは、(A) で openid を頼んだときだけです。(E) では、機密クライアントならアプリの証明(シークレットや証明書)を付けます。ここまでは OAuth 2.0 の認可コードフローと同じです。
ID トークンに入るもの
OpenID Connect Core の2章が決めている、ID トークンのクレームです。
| クレーム | 意味 |
|---|---|
| iss | 必須。ID トークンを出した OP |
| sub | 必須。利用者の ID。その OP の中で一意で、使い回されない |
| aud | 必須。RP のクライアント ID を含む(ほかの値が入ることもある) |
| exp・iat | 必須。期限と、発行した時刻 |
| nonce | 要求に nonce を入れたら、OP は必ず ID トークンに入れる |
| auth_time | 利用者が認証した時刻。max_age を指定したときなどは必須 |
| acr・amr | 任意。認証の強さと、使った方法 |
| azp | 任意。入っている場合は、その相手のクライアント ID |
アプリが確かめること
OpenID Connect Core の 3.1.3.7 は、RP が ID トークンを受け取ったときに確かめることを決めています。考え方はJWT の検証の記事と同じで、あて先が「API」ではなく「アプリ自身」になります。
あて先(aud)に自分のクライアント ID が入っていなければ、そのトークンは受け入れません。nonce を送ったなら、ID トークンの nonce が同じ値かも確かめます。署名の方式は、登録で別に決めていなければ RS256 が既定です。なお仕様では、トークンエンドポイントから TLS で直接受け取った ID トークンは、TLS の確認で署名の検証に代えてもよいとされています。ただ、Microsoft のドキュメントは、ID トークンは手で確かめず、ライブラリで確かめるよう勧めています。
OpenID Connect には、アクセストークンで利用者の情報を返す UserInfo という窓口もあります。profile や email のスコープを頼むと、名前やメールアドレスを返します。Entra ID の場合、UserInfo は Microsoft Graph にあり、Microsoft のドキュメントは、ID トークンの情報は UserInfo の情報を含んでいるので、通信を減らすためにも ID トークンから情報を取るよう勧めています。
誰が何を持つか
ここからは Microsoft の場合です。Entra ID を OP にして、ASP.NET Core の Web アプリ(機密クライアント)にサインインする場面を例にします。
- 利用者(ブラウザ):パスワードなどは Entra ID にだけ入力します。ブラウザには、Entra ID のサインインのセッション(ほかのアプリへのシングルサインオンに使う)と、サインインが終わったあとのアプリのセッションの Cookie ができます。
- アプリ(サーバ):クライアント ID とシークレットを持ちます。サインインのたびに
stateとnonceを作って覚えておき、届いた ID トークンを確かめてから、中身を使います。 - Entra ID:ID トークンに署名する秘密鍵を持ち、公開鍵と設定を公開しています。
- Graph(UserInfo):利用者の情報を返しますが、多くの場合は ID トークンで足ります。
全体の流れ(Entra ID・Web アプリの場合)
図8のものが、どこで作られ、どこを通って、どこで使われるのかを、一歩ずつ並べます。番号は2つの図を通しての番号です。
準備(最初に1回):①〜⑧
| 手順 | 誰が・どこで | 何をするか | 鍵(物)はどうなるか・使う鍵 |
|---|---|---|---|
| ① | 管理者・管理センター | アプリを Web として登録し、リダイレクト URI(例:https://localhost:5001/signin-oidc)を書く | クライアント ID が決まる(ID トークンの aud に入る値) |
| ② | 管理者・管理センター | クライアントシークレットを作る | Entra ID が値を作り、照合用の情報を持つ |
| ③ | 管理センター → 管理者 | シークレットの値が、画面に一度だけ表示される | 秘密の値が管理者の手元を通る |
| ④ | 管理者・サーバで操作 | アプリの設定に、テナント ID・クライアント ID・コールバックのパス・資格情報を書く | 秘密の値がサーバに置かれる |
| ⑤ | アプリ → Entra ID | 起動時に、OpenID Connect の設定(Discovery)を取りに行く | ― |
| ⑥ | Entra ID → アプリ | 設定が返る。issuer・各窓口・jwks_uri が入っている | 期待する発行者が分かる |
| ⑦⑧ | アプリ ⇄ Entra ID | 公開鍵の一覧を取り、キャッシュする | 公開鍵がアプリに届く。秘密鍵は渡らない |
シークレットの代わりに証明書を使う場合の鍵の動きは、クライアントクレデンシャルの記事の図6と同じです。⑤〜⑧は、Microsoft.Identity.Web が自動で行います。
サインイン(利用者が来るたび):⑨〜㉑
| 手順 | 誰が・どこで | 何をするか | 鍵(物)はどうなるか・使う鍵 |
|---|---|---|---|
| ⑨ | 利用者 → アプリ | サインインが要るページを開く | ― |
| ⑩ | アプリ | この要求のための state と nonce を作り、覚えておく | 2つの値がアプリにできる |
| ⑪ | アプリ → ブラウザ | scope=openid profile・state・nonce(と PKCE の code_challenge)を付けて、Entra ID のサインイン画面へ転送する | state と nonce がブラウザを通って Entra ID へ |
| ⑫ | ブラウザ → Entra ID | /authorize に要求を送る | ― |
| ⑬ | 利用者・Entra ID | サインインし(多要素認証など)、同意する | パスワードなどは Entra ID にだけ入る |
| ⑭ | Entra ID → アプリ | 認可コードと state が、ブラウザ経由でアプリの /signin-oidc に届く | コードが動く(ID トークンではない) |
| ⑮ | アプリ | state が⑩で覚えた値と同じかを確かめる | ― |
| ⑯ | アプリ → Entra ID | 認可コード・シークレット・code_verifier(PKCE の値)を付けて、トークンを頼む | シークレットを送る |
| ⑰ | Entra ID | aud(アプリのクライアント ID)・nonce・sub・oid などを入れた ID トークンを作る | Entra ID の秘密鍵で署名 |
| ⑱ | Entra ID → アプリ | ID トークン(API を呼ぶならアクセストークンも)を返す | ID トークンがアプリに届く |
| ⑲ | アプリ | ID トークンを確かめる。署名・iss・aud・exp・nonce(⑩と同じか) | Entra ID の公開鍵で検証 |
| ⑳ | アプリ | アプリのセッションを作り、Cookie を発行する | アプリのセッションの Cookie がブラウザへ |
| ㉑ | アプリ → 利用者 | ページを返す(例:「○○さん」と表示) | ― |
ブラウザを通るのは、⑪〜⑭の要求と認可コードだけです。ID トークンは⑯〜⑱でサーバどうしで受け取り、⑲で確かめます。⑳でアプリのセッションができたあとは、ページを開くたびに Entra ID に行く必要はありません。Microsoft のドキュメントは、Web アプリのセッションの長さを ID トークンの期限(既定は1時間)に合わせることが多い、と説明しています(セッションとトークンの記事も参照)。
サインアウト:❶〜❽
サインアウトの流れも、図10と区別するため黒丸(❶❷…)の番号で並べます。
| 手順 | 誰が・どこで | 何をするか | 図10との違い |
|---|---|---|---|
| ❶❷ | 利用者・アプリ | サインアウトを押す。アプリは自分のセッション(Cookie)を消す | ⑳で作ったものを消す |
| ❸❹ | アプリ → ブラウザ → Entra ID | post_logout_redirect_uri を付けて、Entra ID のログアウトの窓口へ転送する | ⑪⑫の代わりに /logout |
| ❺ | Entra ID | Entra ID 側のサインインのセッションを終える | ⑬でできたセッションを終える |
| ❻❼ | Entra ID → ほかのアプリ | フロントチャネルログアウト URL を呼ぶ。ほかのアプリはセッションを消し、200 を返す | 設定したアプリだけ |
| ❽ | Entra ID → ブラウザ | post_logout_redirect_uri へ戻す | ― |
Microsoft のドキュメントは、サインアウトでは「Entra ID のログアウトの窓口へ転送する」ことと「アプリの Cookie を消すかセッションを終える」ことの両方を行うよう書いています。どちらかが抜けると、利用者はサインインしたままになり、次に使うときにサインインを求められないことがあります。
Microsoft(Entra ID)での手順
図9・図10の番号に沿って、実際の操作と設定を見ます。
アプリを登録する(図9の①〜③)
①では、Entra ID ›› App registrations ›› New registration でアプリを登録します。サインインできるアカウントの種類(自分の組織だけか、ほかの組織もか)を選び、リダイレクト URI のプラットフォームを Web にして、https://localhost:5001/signin-oidc のように入力します。概要の画面に出る「Application (client) ID」と「Directory (tenant) ID」が、④で使う値です。
②③のシークレットは、Certificates & secrets ›› New client secret で作ります(本番では証明書が勧められています)。Authentication の画面には「ID tokens(インプリシットとハイブリッドのフロー用)」というチェックもありますが、Microsoft のドキュメントは、認可コードフローが推奨で、このチェックはそれが必要な場合だけ入れるよう書いています。
アプリの設定に書く(図9の④〜⑧)
④では、appsettings.json に Entra ID の情報を書きます。
④ appsettings.json(Microsoft のドキュメントの例)
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-client-id",
"CallbackPath": "/signin-oidc"
}
}
TenantId は、サインインできるアカウントを決めます。自分のテナント ID なら、その組織の人だけです。CallbackPath は⑭で認可コードが届く場所で、①で登録したリダイレクト URI と合わせます。シークレットや証明書は、このほかに設定します。
④ Program.cs(Microsoft のドキュメントの例)
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
var builder = WebApplication.CreateBuilder(args);
// Add authentication
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi() // Optional: if calling APIs
.AddInMemoryTokenCaches(); // For production, use distributed cache
// Add Razor Pages or MVC
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI(); // Adds sign-in/sign-out UI
var app = builder.Build();
// Configure middleware
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication(); // Add authentication middleware
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
AddMicrosoftIdentityWebApp で、OpenID Connect のサインインが組み込まれます。⑤〜⑧の設定と公開鍵の取得、⑩〜⑲のやり取りと ID トークンの確かめ、⑳のセッションの Cookie は、このライブラリと ASP.NET Core が行います。API も呼ぶなら、例のように EnableTokenAcquisitionToCallDownstreamApi を付けます。
ページを守り、利用者の情報を使う(図10の⑨・⑳㉑)
⑨の「サインインが要るページ」は、[Authorize] 属性で決めます。サインインしていない利用者がこのページを開くと、⑩以降の流れが始まります。
⑨㉑ サインインが要るページ(Microsoft のドキュメントの例)
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; [Authorize] // Require authentication public class IndexModel : PageModel { public void OnGet() { var userName = User.Identity?.Name; var userEmail = User.FindFirst("preferred_username")?.Value; } }
サインインのあとは、ID トークンのクレームを User から読めます。ただし、表示に使う値と、利用者を見分けるのに使う値は分けます。
| クレーム | Entra ID での中身と使い方 |
|---|---|
| aud | アプリのクライアント ID。合わなければ受け入れない |
| nonce | 最初の要求に入れた値と同じ。違えば受け入れない |
| oid | 利用者の変わらない ID。同じテナントなら、アプリが違っても同じ値 |
| sub | アプリごとに違う、変わらない ID |
| tid | サインインしたテナントの ID |
| name | 表示名。変わることがあり、表示だけに使う(profile スコープで入る) |
| preferred_username | ユーザー名。変わることがあり、判断には使わない(profile スコープで入る) |
Microsoft のドキュメントは、メールアドレスや UPN などは変わったり、ほかの人に使い回されたりすることがあるので、利用者を見分けるのに使わないよう書いています。利用者ごとにデータを持つときは、sub か oid(必要なら tid と組み合わせて)をキーにします。
サインアウトする(図11の❸❹)
Microsoft.Identity.Web.UI を使うと、サインアウトのリンクが用意されています。手で作る場合は、Discovery の設定にある end_session_endpoint(ログアウトの窓口)へ転送します。
❸❹ ログアウトの要求(Microsoft のドキュメントの例)
GET https://login.microsoftonline.com/common/oauth2/v2.0/logout?
post_logout_redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
post_logout_redirect_uri は、サインアウトのあとに戻る URL で、アプリの登録にあるリダイレクト URI のどれかと一致している必要があります。ほかのアプリも一緒にサインアウトさせたいときは、それぞれのアプリの登録にフロントチャネルログアウト URL を設定します(❻❼)。
ID トークンでしてはいけないこと
| してはいけないこと | 理由(Microsoft のドキュメント) |
|---|---|
| ID トークンで API を呼ぶ | ID トークンはサインインの証拠で、API への許可の証拠はアクセストークン。ID トークンを認可に使わない |
| メールアドレスやユーザー名で利用者を見分ける | 変わったり、使い回されたりする。sub や oid を使う |
| 確かめずに中身を使う | 機密クライアントは ID トークンを確かめる。手で書かずにライブラリを使う |
| ブラウザだけのアプリで、確かめたから安全と考える | 端末やブラウザを触れる人は確かめを飛ばせるので、SPA やネイティブアプリでは確かめてもあまり意味がない |
うまくいかないとき
| エラー | 意味 | 確かめること |
|---|---|---|
| AADSTS50011 | リダイレクト URI が登録と合わない | アプリの登録のリダイレクト URI と、CallbackPath(既定は /signin-oidc) |
| AADSTS700016 | アプリが見つからない | appsettings.json の ClientId が、登録の Application (client) ID と同じか |
| response_type が許可されていない | ID トークンを認可の窓口から直接頼んだが、アプリで有効になっていない | 認可コードフローを使っているか。ハイブリッドフローが本当に要るときだけ「ID tokens」を有効にする |
| access_denied | 利用者が同意しなかった、またはキャンセルした | 同意しないと進めないことを、アプリの画面で伝える |
| Authority の設定エラー | Instance か TenantId がない、または正しくない | Instance が https://login.microsoftonline.com/ か。TenantId が正しいか |
企業での例
| 場面 | やること | 押さえどころ |
|---|---|---|
| 社内ポータルに Entra ID でサインインさせる | Web アプリとして登録し、Microsoft.Identity.Web でサインインを組み込む | TenantId を自社のテナントにして、社外のアカウントを入れない |
| サインインした人の Outlook の予定も表示する | ID トークンに加えて、Graph のアクセストークンももらう | ID トークンは「誰か」、アクセストークンは「何をしてよいか」。役目を混ぜない |
| 利用者ごとに設定を保存する | oid と tid(または sub)をキーにする | メールアドレスをキーにすると、名前の変更や再利用で取り違える |
| 複数の社内アプリをまとめてサインアウトさせる | 各アプリにフロントチャネルログアウト URL を設定する | アプリのセッションと Entra ID のセッションの両方を終える |
まとめ
| 項目 | 内容 |
|---|---|
| どんなもの | OAuth 2.0 の上に乗る ID の層。「誰がサインインしたか」を ID トークンで伝える |
| OAuth 2.0 との違い | 流れは認可コードフローのまま。scope に openid を入れると ID トークンが届く |
| ID トークンが要る理由 | アクセストークンは API あてで、誰あてか・今サインインしたかが分からない。ID トークンはアプリあてで署名つき |
| 確かめること | iss・aud(自分のクライアント ID)・署名・exp・nonce。ライブラリに任せる |
| Entra ID の場合 | Web アプリとして登録し、Microsoft.Identity.Web にテナント ID・クライアント ID・コールバックのパスを設定する |
| 使い方 | 確かめたらアプリのセッションを作る。利用者のキーは oid・sub。表示名やメールは表示だけに使う |
| サインアウト | アプリのセッションを消し、Entra ID のログアウトの窓口へ転送する |
参考にした資料
この記事は、次の仕様と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。
- OpenID Connect Core 1.0(OpenID Foundation):OAuth 2.0 の上の ID の層であること、ID トークンのクレーム(2章)、認証の要求のパラメータ(3.1.2.1)、ID トークンの確かめ方(3.1.3.7)、Pre-defined のクレームをスコープで頼めること(5章)
- RFC 9700: Best Current Practice for OAuth 2.0 Security(IETF):アクセストークンを差し込む攻撃と、OAuth だけでは見分けられないこと、ID トークンの at_hash と nonce での対策(4.6)、インプリシットフローを使わないこと(2.1.2)
- User Authentication with OAuth 2.0(oauth.net):アクセストークンを持っているだけではサインインの証拠にならないこと、アプリあての情報が要ること
- OpenID Connect on the Microsoft identity platform(Microsoft Learn):Entra ID の窓口、Discovery の例、サインインの要求のパラメータ、ID トークンの確かめ方とライブラリの推奨、サインアウトとフロントチャネルログアウト
- ID tokens in the Microsoft identity platform・ID token claims reference(Microsoft Learn):名札のたとえ、ID トークンで API を呼ばない・認可に使わないこと、期限が既定で1時間、確かめるのは機密クライアント、各クレームの意味、利用者を見分けるのに sub・oid を使うこと
- Microsoft identity platform UserInfo endpoint(Microsoft Learn):UserInfo が Graph にあること、ID トークンから情報を取るよう勧めていること
- Quickstart: Sign in users in an ASP.NET Core web app(Microsoft Learn):アプリの登録、appsettings.json と Program.cs、[Authorize]、ID tokens のチェックの扱い、よくあるエラー
- Configurable token lifetimes in the Microsoft identity platform(Microsoft Learn):Web アプリのセッションの長さを ID トークンの期限に合わせることが多いこと
- Microsoft identity platform and OAuth 2.0 authorization code flow(Microsoft Learn):トークンの応答の id_token が openid を頼んだときだけ返ること、Web アプリのシークレットと code_verifier
あとがき
ID トークンが要る理由を調べ直してみて、大事なのは「アクセストークンはアプリあてではない」という一点なのだと分かりました。名札と鍵を分けて考えると、ID トークンで API を呼ばない、アクセストークンでサインインにしない、という決まりも自然に覚えられます。次の記事では、OpenID Connect と SAML、OAuth の違いを並べて整理するつもりです。
コメント