ネイティブアプリから複数のバックエンドサービスを叩くとき、「ログインは1回だけ。でもサービスごとに別々のトークンを持たせたい」をどう実現するか。
その答えが 署名付き JWT(アサーション)を提示して、各サービスにトークンを発行してもらう 仕組みだ。OAuth の世界では RFC 7521 / 7523(および関連する 8693)として標準化されている。この記事では IdP・RP・モバイルアプリの3者がどう連携するのかを、図を交えて噛み砕いて説明する。
登場人物(3者)
| 役割 | 正体 | 仕事 |
|---|---|---|
| IdP(Identity Provider) | 統合認証サーバー | 「あなたが誰か」を確認し、身分証となる署名付き JWT を発行する本人確認の元締め。+ RP からの「このユーザーで合ってる?」という照会に答える |
| RP(Relying Party / サービス) | 各バックエンド(例: 経費・マスタ・店舗 API) | IdP の身分証を信頼し、自分専用のアクセストークンを自分で発行する。各 RP が小さな認可サーバーを兼ねる |
| モバイルアプリ(public client) | スマホアプリ | ユーザーの手元で動く。クライアントシークレット(client secret)を安全に持てないのが最大の制約 |
ポイントは 「IdP は1つ、RP は複数。トークンを発行するのは IdP ではなく各 RP」 という構図だ。ユーザーは IdP に1回ログインして「身分証(署名付き JWT)」を 1枚だけ 受け取る。そのあと、その同じ身分証を各サービスの窓口に持ち込み、サービスごとに入館証(アクセストークン)を発行してもらう(RP ごとに別の JWT をもらい直すわけではない)。この“1枚の身分証を見せて、各窓口で入館証を作ってもらう”作業が今回の主題だ。
⚠️ よくある誤解: 「IdP が各サービス用のトークンをまとめて発行する」と思いがちだが、実際に access/refresh トークンを発行するのは各 RP だ。IdP は身分証(JWT)を出すのと、ユーザー照会に答えるだけ。
なぜ「身分証 → 各サービスで発行」なのか
素朴に考えると「IdP からもらったトークンを、そのまま全サービスに使い回せばいいのでは?」と思う。が、これは筋が悪い。
- スコープが広すぎる — 1枚で全サービスに通るトークンは、漏れたときの被害が全サービスに及ぶ。
- 宛先が曖昧 — 経費サービスに渡したトークンを店舗サービスが受け取れてしまうと、トークンの“使い回し攻撃”が成立する。
- 責務が混ざる — 「身分証(あなたが誰か)」と「入館証(このサービスで何ができるか)」は本来別物。署名鍵やトークン管理を各サービスが自前で完結できる方が疎結合だ。
そこで 「身分証(JWT)を各サービスに見せて、サービスごとに入館証を発行してもらう」 という設計にする。身分証=アサーション(assertion)、それを受けて各 RP が自分のトークンを発行する。
関連する RFC の役割分担
「署名付きアサーションでトークンをもらう」仕組みは、RFC が階層になっている。
| RFC | 通称 | grant_type | ひとことで言うと |
|---|---|---|---|
| RFC 7521 | アサーションフレームワーク | (枠組み) | 「アサーションを使ってトークンを得てよい」という一般ルール。土台。 |
| RFC 7523 | JWT Profile(JWT Bearer) | urn:ietf:params:oauth:grant-type:jwt-bearer | 「署名付き JWT を1枚提示 → 受け取った側がアクセストークンを発行」。今回の仕組みに最も近い。 |
| RFC 8693 | Token Exchange | urn:ietf:params:oauth:grant-type:token-exchange | 「中央の認可サーバーがトークンをトークンに交換。宛先や権限を絞れる。委任・代理も表現」。中央集約型で、今回のような「各 RP が自分で発行」とは形が違う。 |
例えるなら:
- RFC 7521 は「身分証を見せれば入館証を発行してもらえる、という館のルール」
- RFC 7523 は「運転免許証(=署名付き JWT) を各フロアの受付に出すと、そのフロアが入館証を発行してくれる」← 今回これ
- RFC 8693 は「中央の総合受付が、今の入館証を別フロア用に引き換えてくれる」← 中央集約。今回とは別形
今回扱う「各 RP が JWT アサーションを受けて自分のトークンを発行する」方式は、意味的には RFC 7523 が一番近い。8693 のような中央交換ではない。
RFC 7523 — 署名付き JWT を出して、RP にトークンを発行してもらう
モバイルアプリが手にした署名付き JWT(IdP が秘密鍵で署名した身分証)を、各サービス(RP)の /token エンドポイントに「アサーション」として提出する。受け取った RP がトークンを発行する。
POST /token HTTP/1.1
Host: epm.example.com ← IdP ではなく「経費サービス(RP)」の /token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJSUzI1NiIs...(IdPが署名したJWT本体)
&scope=read write
RP(経費サービス)側の処理:
assertion(JWT)の 署名を検証(IdP の公開鍵 = JWKS で。改ざんされていないか)exp(有効期限)やiss(発行者)が妥当か確認- 同じ JWT を assertion として、今度は RP 自身が IdP の
/tokenに提示(=もう一段の RFC 7523)。IdP がaccess_token + refresh_tokenを返せば、その JWT の IdP ユーザーが確定する - 確定した IdP ユーザーに対応する「RP ローカルユーザー」を作成/解決する(初回ならアカウントを作る=JIT プロビジョニング、2回目以降は紐付け済みの既存ユーザーを引く)
- その RP ローカルユーザーのアクセストークン(+リフレッシュトークン)を発行してクライアントに返す
ここで取り違えやすいのが発行の根拠だ。RP がローカルトークンを返すのは「手順1の署名検証に通ったから」ではなく、**「手順3で IdP がトークンを返した(=IdP がそのユーザーを認めた)という事実」**を根拠にしている。署名検証(手順1)は予備的な fail-fast チェックで、JWT の真正性は示すが「失効していないか」までは保証しない。最終的な本人性・有効性の確定は、IdP への往復(7523-b)が「IdP からの正常なトークン応答」という形で担う。だから署名検証だけで済ませず、わざわざ RP→IdP を往復する。
ユーザー確認も RFC 7523 — 7523 を「2段」で使う
上の手順3「ユーザーの確定」は、独自の問い合わせ API ではなく RP→IdP でもう一度 RFC 7523(jwt-bearer)を回しているのがポイント。同じ JWT Bearer Grant を client→RP と RP→IdP の2段で再帰的に使う。
そして 「IdP が RP に access/refresh を返した」こと自体が「この JWT のユーザーは正当」という確定を意味する。つまりユーザー確認は別プロトコルではなく、7523 の正常なトークン応答で表現されている。だから後述のフロー図でも、RP→IdP の往復は RFC 7523 の範囲の中に入る。
aud(audience)は IdP — 1枚の JWT を全 RP で使い回すモバイルアプリは1回のログインで JWT を1枚だけ受け取り、その同じ JWT を各 RP に提示する。1枚を複数 RP で使い回すため、JWT の
audは**特定の RP ではなく IdP(または全 RP を含む共通の値)**になる(audを1つの RP に固定すると、その JWT は他の RP では使えなくなる)。この前提では、
RP→IdP(7523-b)こそがaud=IdPの教科書どおりの RFC 7523 だ。IdP は「自分が署名し、自分をaudとする JWT」を受け取って検証し、ユーザーを確定する。一方app→RP(7523-a)では RP は JWT のaud(=IdP)ではないので、RP は「IdP 発行の身分証」を預かって IdP に取り次ぐブローカーとして振る舞う(RP のローカル署名検証は fail-fast の予備チェックで、最終的な確定は IdP への往復が担う)。モバイルアプリは終始 JWT を中継するだけで、audにはならない。
RP は confidential client として認証する — アサーションとは別レイヤー
RP→IdP の往復では、ユーザーを表す JWT(
assertion)に加えて、RP 自身のclient_id/client_secretをトークンエンドポイントに送る設計が一般的だ。これは RFC 6749 のクライアント認証で、アサーション(RFC 7523 の認可グラント)とは直交する別レイヤーにあたる。RFC 7523 §2.1 も「confidential client は自身のクレデンシャルでトークンエンドポイントに認証する」と前提しており、両者を同時に送るのは逸脱ではなく標準どおりだ。効果は 「登録済みの RP にだけトークンを返す」 こと。先に触れた「RP↔IdP のトラスト」は、この
client_id/client_secret認証として具体化される。public client であるモバイルアプリ(app→RP)は secret を持てないのでクライアント認証なし、サーバー間の RP→IdP は confidential client として認証あり、という非対称も標準の範囲に収まる。
最終的に発行されるのは「RP ローカルユーザー」のトークン(手順4〜5)
確定するのは IdP 上のユーザーだが、モバイルが最後に受け取るのは RP 内に作られたローカルユーザー(IdP ユーザーに紐付く)のトークンだ。RP は IdP ユーザーをそのまま使うのではなく、**自分のドメインのユーザーへマッピング(JIT プロビジョニング/アカウントリンク)**してから発行する。これにより各 RP はロール・権限・プロフィールなどを自分のユーザーモデルで完結して管理できる。なおこのローカルユーザー作成は RFC の範囲外(RP のアプリ側ロジック) で、7523 のトークン応答を受けて RP が行う処理だ。
返ってくるもの:
{
"access_token": "(経費サービスが、RPローカルユーザー向けに発行したトークン)",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "..."
}
ポイント:
- 発行者は RP。各サービスが自分の DB にトークンを持つので、以降の API 呼び出しは RP がローカルで検証できる(毎回 IdP に問い合わせない)。
- クライアントシークレットが不要。「IdP が署名した JWT を持っている」こと自体が本人性の証明になるので、クライアントシークレットを持てないモバイル(public client)でも使える。
- RP→IdP の確定もトークン発行時の1回だけ。しかもそれ自体が RFC 7523 のトークン取得なので、独自プロトコルを持たずに「2段の 7523」で完結する。
モバイルアプリでの全体フロー
3者がどう動くかを通しで見る。色の付いた帯がそれぞれの RFC の担当範囲で、緑の帯(②)が今回の主題 RFC 7523 の範囲だ。緑帯の中に client→RP と RP→IdP の2つの 7523 呼び出しが入れ子で含まれる点に注目してほしい。
帯の色と担当範囲(凡例):
| 帯 | フェーズ | 担当する RFC |
|---|---|---|
| 🟦 青 | ① ログイン | RFC 6749(Authorization Code)+ RFC 7636(PKCE) |
| 🟩 緑 | ② JWT 提示 → RP 発行(client→RP と RP→IdP の 2段の 7523) | RFC 7523(JWT Bearer Grant)← 本記事の主題 |
| 🟧 橙 | ③ API 呼び出し | RFC 6749(Bearer トークンでのリソースアクセス) |
緑帯の中の **7523-b(RP→IdP)が「ユーザー確定」**にあたる。独自の確認 API ではなく、IdP が 7523 のトークン応答を返したこと=ユーザーが正当、と解釈する。だから「ユーザー確認」も RFC 7523 の範囲に収まる。
要点:
- ① のログインで PKCE が必須 — モバイルは秘密鍵を持てないため、「認可コードの横取り対策」として PKCE(S256) で“このアプリが確かにコードを要求した本人”を証明する。これは RFC 6749 + 7636 の世界。
- ② が今回の主役 = RFC 7523 の範囲(しかも2段) — 身分証(JWT)を client→RP(7523-a)で提示し、RP は RP→IdP(7523-b)で同じ JWT を IdP に出して IdP ユーザーを確定する。IdP がトークンを返す=ユーザー正当。両方の往復とも同じ JWT Bearer Grant で、ここまでが 7523 の担当。
- 確定後、RP は IdP ユーザーに対応する RP ローカルユーザーを作成/解決し、そのローカルユーザーのトークンを発行する(JIT プロビジョニング。RFC の範囲外で、RP のアプリ側ロジック)。モバイルが最終的に持つのは RP ローカルユーザーのトークン。
- ③ は普通の Bearer トークン — ここはもう 7523 ではなく、RFC 6749 のリソースアクセス。RP は自分が発行したトークンを検証するだけ(ローカル)。
RFC 7523 の役割は IdP 側と RP 側で違う — 対比で整理
同じ RFC 7523(JWT Bearer Grant)でも、JWT を受け取る側が RP か IdP かで役割がまるで違う。混乱しやすいので、2つのホップ(app→RP の 7523-a と RP→IdP の 7523-b)を受ける側の立場で対比しておく。前提として、アプリが持つ JWT は1枚で aud は IdP/共通、それを両ホップで使い回す。
| 観点 | RP 側(app→RP の 7523-a を受ける) | IdP 側(RP→IdP の 7523-b を受ける) |
|---|---|---|
| JWT に対する立場 | 検証者。他者(IdP)の署名を JWKS で確認し、IdP に取り次ぐ | 発行者。自分が署名した JWT を検証する |
この /token の役割 | アプリに対する認可サーバー(自分のトークンを発行) | RP に対する認可サーバー(ユーザー確定のトークンを返す) |
aud(audience) | aud ≠ RP(aud は IdP/共通)。RP は宛先ではなく、預かって IdP に取り次ぐ | aud = IdP に一致 → 教科書どおり受理 |
| クライアント認証 | アプリは public client → なし(PKCE で補完) | RP は confidential client → client_id / client_secret あり |
| この hop が確認すること | JWT の真正性・完全性(本物の IdP 由来か。予備チェック) | 失効していないか(liveness) + 登録済み RP か |
| 7523 が果たす意味 | フェデレーテッド ID → ローカルトークンの発行(IdP 発行アサーションのブローカー) | 自己発行 JWT の検証=ユーザーの有効性・本人確定 |
| 返すもの | RP ローカルユーザーの access/refresh(最終的にアプリが持つ) | IdP の access/refresh(RP に対する「確定の証」) |
| RFC 解釈 | jwt-bearer グラントの形だが RP は aud ではない →「IdP 発行アサーションのブローカー」 | 教科書どおりの jwt-bearer 認可グラント(aud=IdP、発行者自身が受ける) |
ひとことで言えば、RP 側の 7523 は「他者の身分証を信じて自分の入館証を発行する」入口であり、IdP 側の 7523 は「自分が出した身分証を検証してユーザーを確定する」確認窓口だ。同じプロトコルを、検証者の立場と発行者の立場という真逆の役割で2回使っているのが、この構成の肝になる。
public client の落とし穴 — 「誰が RP の /token を呼ぶか」
ここが設計上いちばん悩ましい点だ。
本来この仕組みは、トークンが欲しい側のバックエンド(confidential client)が秘密鍵を持って RP の /token を呼ぶサーバー間パターンが基本だった。ところがモバイルは public client で秘密鍵を安全に保持できない。
解決策は BFF / Token Broker(サーバー側の仲介役) を置くことだ。BFF は Backend for Frontend(フロントエンド専用に用意するバックエンド)の略で、特定のフロント(ここではモバイルアプリ)だけに対応するサーバー側の薄い層を指す。Token Broker はそのうちトークンの取得・保管・受け渡しを担う役割を強調した呼び方だ。要は 「秘密鍵を安全に持てるサーバーを1枚かませ、そこに RP の /token 呼び出しを肩代わりさせる」 という発想である。
- 発行ロジックそのものは新規開発不要(各 RP に JWT アサーションを受け付ける grant が実装済みであれば、それを使い回せる)。
- 新しく設計するのは 「誰が RP の
/tokenを呼ぶか = クライアント構成・BFF の配線」 だけ。 - トークンは localStorage/sessionStorage に置かない(XSS で漏洩する)。Token Broker・OS のセキュアストレージ・in-memory + httpOnly Cookie のいずれかを使う。
「独自命名」を標準に寄せる意味
独自の grant_type 名(例: urn:ietf:params:oauth:grant-type:assertion-grant)を使っているシステムは少なくない。機能は RFC 7523 とほぼ等価(IdP 発行の JWT を RP の /token に提示 → RP がトークン発行)でも、名前が IANA 登録名でないため「独自プロトコル」に見えてしまう。
標準名(jwt-bearer)へ寄せると:
- 各言語の OAuth/OIDC ライブラリが標準対応で使える(独自実装を抱え込まない)
- 外部 IdP・API ゲートウェイ・SaaS との相互運用がしやすい
- デスクトップ/モバイルのクライアントが標準フローに乗る
実装の現実解は段階的だ。いきなり認可サーバーを別ライブラリへ全面移行する必要はない——まずは既存の AS にカスタムグラントを足す、が基本線になる。
| 段階 | やること | コスト |
|---|---|---|
| 最小移行(推奨) | 既存の認可サーバーにカスタムグラントを追加し、grant_type・パラメータを RFC 7523(jwt-bearer)相当へ寄せる。中身は既存実装のままでも「標準準拠の見た目」になり、クライアントは標準ライブラリで叩ける | 小 |
| ライブラリ移行(必要時のみ) | 既存 AS では表現しきれない要件にぶつかったときに限り、認可サーバーを Authlib 等へ寄せ、7523・PKCE・OIDC discovery・introspection をまとめて標準対応 | 大(コア差し替え) |
| 8693 を採用 | 中央でトークンを束ねる/委任・代理まで含めて標準化したいときのみ。専用 IdP 導入 or 自作が前提 | 大 |
内部完結で十分なら、機能上は現行のままでも問題ない。標準化は基盤刷新・IdP 整備のタイミングと合わせて判断するのが現実的だ。
既存スタック(例: Django)からの現実解
たとえば認可サーバーに django-oauth-toolkit(
oauthlibベース)、ログイン側に django-allauth を使っている場合、両者をそのまま活かしたまま最小移行できる。allauth は「認証(ログイン)」、django-oauth-toolkit は「OAuth2 認可サーバー」と役割が分かれており、RFC 7523 のjwt-bearerで不足するのは後者だけだ。しかもそれは django-oauth-toolkit を捨てずにカスタムグラントとして追加できる(oauthlib + DOT を RFC 7523 へ拡張した先例もある)。Authlib への差し替えは AS を丸ごと入れ替える話なので、本格的な RFC 8693 Token Exchange などライブラリの限界にぶつかってから検討すれば十分だ。
まとめ
- IdP は身分証(署名付き JWT)を発行し、各 RP はその身分証を見て自分専用のトークンを自分で発行する。トークン発行者は IdP ではなく 各 RP。
- IdP の役割は (1) 身分証(JWT)の発行元、(2) RP→IdP の 7523 でトークンを返してユーザーを確定する、の2点。
- RFC 7521 が枠組み、RFC 7523 が「JWT を提示 → 受け取った側がトークン発行」で今回に最も近い。8693(中央交換)は形が違う。
- この方式は 7523 を2段(client→RP / RP→IdP)で使う。ユーザー確認も独自 API ではなく 7523 のトークン応答で表現するので、フロー全体が 7523 の範囲に収まる。
- 確定後、RP は IdP ユーザーに対応する RP ローカルユーザーを作成/解決(JIT プロビジョニング)し、そのローカルユーザーのトークンを発行する。モバイルが最終的に持つのは RP ローカルユーザーのトークン(この作成処理は RFC 範囲外のアプリ側ロジック)。
- モバイルは ① Authorization Code + PKCE でログイン → ② IdP の JWT を各 RP の /token に提示し RP がローカルユーザーのトークンを発行 → ③ Bearer で API 呼び出し。
- public client は秘密鍵を持てないので BFF / Token Broker が RP の
/tokenを代行で呼ぶ。新規設計は「発行ロジック」ではなく「誰が呼ぶか」。 - 独自命名を 標準(7523)に寄せると、ライブラリ・相互運用・クライアント実装が楽になる。


