本文へスキップ
hdknr blog
戻る

モバイルアプリの「1回ログイン → サービスごとのトークン」を支える仕組み — OAuth RFC 7523 入門

ネイティブアプリから複数のバックエンドサービスを叩くとき、「ログインは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つ・RP は複数という構図を示した図。中央上の IdP が署名付き JWT を発行し、その下のモバイルアプリがログインして JWT を受け取り、下段に並んだ経費・マスタ・店舗の各 RP に JWT を提示して、各 RP が自分専用のトークンを発行する。各 RP は RP→IdP の照会でユーザーを確定する

なぜ「身分証 → 各サービスで発行」なのか

素朴に考えると「IdP からもらったトークンを、そのまま全サービスに使い回せばいいのでは?」と思う。が、これは筋が悪い。

そこで 「身分証(JWT)を各サービスに見せて、サービスごとに入館証を発行してもらう」 という設計にする。身分証=アサーション(assertion)、それを受けて各 RP が自分のトークンを発行する。

関連する RFC の役割分担

「署名付きアサーションでトークンをもらう」仕組みは、RFC が階層になっている。

RFC通称grant_typeひとことで言うと
RFC 7521アサーションフレームワーク(枠組み)「アサーションを使ってトークンを得てよい」という一般ルール。土台。
RFC 7523JWT Profile(JWT Bearer)urn:ietf:params:oauth:grant-type:jwt-bearer署名付き JWT を1枚提示 → 受け取った側がアクセストークンを発行」。今回の仕組みに最も近い
RFC 8693Token Exchangeurn:ietf:params:oauth:grant-type:token-exchange中央の認可サーバーがトークンをトークンに交換。宛先や権限を絞れる。委任・代理も表現」。中央集約型で、今回のような「各 RP が自分で発行」とは形が違う。

例えるなら:

今回扱う「各 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(経費サービス)側の処理:

  1. assertion(JWT)の 署名を検証(IdP の公開鍵 = JWKS で。改ざんされていないか)
  2. exp(有効期限)や iss(発行者)が妥当か確認
  3. 同じ JWT を assertion として、今度は RP 自身が IdP の /token に提示(=もう一段の RFC 7523)。IdP が access_token + refresh_token を返せば、その JWT の IdP ユーザーが確定する
  4. 確定した IdP ユーザーに対応する「RP ローカルユーザー」を作成/解決する(初回ならアカウントを作る=JIT プロビジョニング、2回目以降は紐付け済みの既存ユーザーを引く)
  5. その 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→RPRP→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": "..."
}

ポイント:

モバイルアプリでの全体フロー

3者がどう動くかを通しで見る。色の付いた帯がそれぞれの RFC の担当範囲で、緑の帯(②)が今回の主題 RFC 7523 の範囲だ。緑帯の中に client→RPRP→IdP の2つの 7523 呼び出しが入れ子で含まれる点に注目してほしい。

モバイルアプリ・IdP・RP の3者間シーケンス図。①青帯はログインで RFC 6749 の Authorization Code と RFC 7636 の PKCE を使い IdP が署名付き JWT を発行する。②緑帯は RFC 7523 の範囲で、アプリが JWT を RP の /token に提示(7523-a)、RP が JWKS で署名検証し、同じ JWT を IdP の /token に提示(7523-b)して access/refresh を受け取りユーザーを確定、その後 RP ローカルユーザーを作成してトークンを発行する。③橙帯は RFC 6749 の Bearer トークンによる API 呼び出し

帯の色と担当範囲(凡例):

フェーズ担当する RFC
🟦 青① ログインRFC 6749(Authorization Code)+ RFC 7636(PKCE)
🟩 緑② JWT 提示 → RP 発行(client→RP と RP→IdP の 2段の 7523RFC 7523(JWT Bearer Grant)← 本記事の主題
🟧 橙③ API 呼び出しRFC 6749(Bearer トークンでのリソースアクセス)

緑帯の中の **7523-b(RP→IdP)が「ユーザー確定」**にあたる。独自の確認 API ではなく、IdP が 7523 のトークン応答を返したこと=ユーザーが正当、と解釈する。だから「ユーザー確認」も RFC 7523 の範囲に収まる。

要点:

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 ≠ RPaud は 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(サーバー側の仲介役) を置くことだ。BFFBackend for Frontend(フロントエンド専用に用意するバックエンド)の略で、特定のフロント(ここではモバイルアプリ)だけに対応するサーバー側の薄い層を指す。Token Broker はそのうちトークンの取得・保管・受け渡しを担う役割を強調した呼び方だ。要は 「秘密鍵を安全に持てるサーバーを1枚かませ、そこに RP の /token 呼び出しを肩代わりさせる」 という発想である。

BFF / Token Broker パターンの図。モバイルアプリ(秘密鍵を持てない)がログイン結果の JWT を BFF / Token Broker(秘密鍵を安全に保持)に渡し、BFF が各 RP の /token に JWT を提示してトークンを取得、httpOnly Cookie 等で安全にアプリへ橋渡しする。新規開発は発行ロジックではなく『誰が /token を呼ぶか』の配線だけで、トークンは localStorage に置かない

「独自命名」を標準に寄せる意味

独自の grant_type 名(例: urn:ietf:params:oauth:grant-type:assertion-grant)を使っているシステムは少なくない。機能は RFC 7523 とほぼ等価(IdP 発行の JWT を RP の /token に提示 → RP がトークン発行)でも、名前が IANA 登録名でないため「独自プロトコル」に見えてしまう。

標準名(jwt-bearer)へ寄せると:

  1. 各言語の OAuth/OIDC ライブラリが標準対応で使える(独自実装を抱え込まない)
  2. 外部 IdP・API ゲートウェイ・SaaS との相互運用がしやすい
  3. デスクトップ/モバイルのクライアントが標準フローに乗る

実装の現実解は段階的だ。いきなり認可サーバーを別ライブラリへ全面移行する必要はない——まずは既存の AS にカスタムグラントを足す、が基本線になる。

段階やることコスト
最小移行(推奨)既存の認可サーバーにカスタムグラントを追加し、grant_type・パラメータを RFC 7523(jwt-bearer)相当へ寄せる。中身は既存実装のままでも「標準準拠の見た目」になり、クライアントは標準ライブラリで叩ける
ライブラリ移行(必要時のみ)既存 AS では表現しきれない要件にぶつかったときに限り、認可サーバーを Authlib 等へ寄せ、7523・PKCE・OIDC discovery・introspection をまとめて標準対応大(コア差し替え)
8693 を採用中央でトークンを束ねる/委任・代理まで含めて標準化したいときのみ。専用 IdP 導入 or 自作が前提

内部完結で十分なら、機能上は現行のままでも問題ない。標準化は基盤刷新・IdP 整備のタイミングと合わせて判断するのが現実的だ。

既存スタック(例: Django)からの現実解

たとえば認可サーバーに django-oauth-toolkitoauthlib ベース)、ログイン側に django-allauth を使っている場合、両者をそのまま活かしたまま最小移行できる。allauth は「認証(ログイン)」、django-oauth-toolkit は「OAuth2 認可サーバー」と役割が分かれており、RFC 7523 の jwt-bearer で不足するのは後者だけだ。しかもそれは django-oauth-toolkit を捨てずにカスタムグラントとして追加できる(oauthlib + DOT を RFC 7523 へ拡張した先例もある)。Authlib への差し替えは AS を丸ごと入れ替える話なので、本格的な RFC 8693 Token Exchange などライブラリの限界にぶつかってから検討すれば十分だ。

まとめ

参考



前の記事
OpenAI Codex で30分ごとにSlack/Gmailを自動巡回して返信下書きを生成するループを設定する
次の記事
M365 Copilot の使い方入門:会社導入後に知っておきたい基本と活用術