第16章 OAuth と OpenID Connect — 外部サービスとの認証連携
この章で学ぶこと
- OAuth 2.0 の 4 役割 (Resource Owner / Client / Authorization Server / Resource Server) を区別できる
- Authorization Code Flow + PKCE が現代の標準であることを理解する
- ID Token (JWT) の構造 (header.payload.signature) と必須 claim を説明できる
- ソーシャルログインの DB 設計ができる
- Implicit Flow が非推奨化された経緯を理解する
第 15 章 認証・認可システムの設計 を理解していること。OAuth は規格が複雑で、本章は実装の入り口に絞っています。
OAuth 2.0 とは何か
OAuth 2.0 は 「自分のサービスのデータを別のサービスに使わせる」 ための認可プロトコルです。よくあるシナリオ:
- 「Google アカウントでログイン」 (Google が認証 → 自社サイトがその ID を受け取る)
- 「自社アプリから Twitter API で投稿」 (自社アプリが Twitter のユーザーデータに代理アクセス)
- 「Slack に GitHub の通知を流す」 (Slack が GitHub の Webhook を受け取る権限を持つ)
- OAuth 2.0 は 認可 (この人は X にアクセスする権利があるか) のプロトコル
- OpenID Connect (OIDC) は OAuth 2.0 の上に乗った 認証 (この人は誰か) の拡張
「Google でログイン」は実は OpenID Connect (= OAuth 2.0 で認可 + ID Token で本人確認)。両者は混同されやすいですが、別の問題を扱っています。
OAuth 2.0 の 4 役割
| 役割 | 例 |
|---|---|
| Resource Owner (ユーザー) | あなた (Google アカウントの持ち主) |
| Client (アプリ) | あなたが使うサードパーティアプリ (写真編集アプリなど) |
| Authorization Server (認可サーバ) | Google のログインサーバ |
| Resource Server (リソースサーバ) | Google Drive API |
「Resource Owner のデータに、Client が、Authorization Server の認可を経て、Resource Server からアクセスする」というのが OAuth の基本構図です。
Authorization Code Flow + PKCE (現代の標準)
OAuth 2.0 にはいくつかの「フロー」(認可手順) がありますが、2026 年現在の標準は Authorization Code Flow + PKCE です (RFC 9700 / OAuth 2.1 draft)。
PKCE は public client で必須 (confidential でも推奨)
OAuth のセキュリティ勧告 RFC 9700 (2025-01) は、Public Client (SPA / モバイル) に PKCE (Proof Key for Code Exchange) を必須 (MUST) としています。Confidential Client (サーバ側) にも推奨 (RECOMMENDED) で、OpenID Connect の confidential client は nonce パラメータで代替できます。さらに OAuth 2.1 draft では認可コードグラントの手順そのものに PKCE が組み込まれ、事実上の標準装備になりました。PKCE は元々 RFC 7636 (2015) で Public Client 向けに導入された仕組みです。
PKCE があると、悪意ある中間者が認可コードを盗んでも、code_verifier を知らないとトークンに交換できません。
Implicit Flow は使わない
かつて SPA 向けに使われていた Implicit Flow は、RFC 9700 (2025-01) で非推奨となり、OAuth 2.1 draft では削除されました。古いチュートリアルや 2018 年以前の解説では Implicit Flow が紹介されていることがありますが、現代では SPA でも Authorization Code Flow + PKCE を使う のが正解です。
理由: Implicit Flow はトークンを URL の fragment で渡すため、ブラウザ履歴 / リファラ漏洩 / リダイレクト先のスクリプトに盗まれるリスクが高い。
OpenID Connect の ID Token (JWT)
OpenID Connect では「この人は誰か」を表す ID Token が発行されます。ID Token の実体は JWT (JSON Web Token, RFC 7519) という標準フォーマットで、3 つの部分が . で区切られた文字列です。
eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTAxNjkxNTM1NTkzMjEzOTI3MzMiLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJleHAiOjE3MzAwMDAwMDAsImlhdCI6MTcyOTk5NjQwMH0.xxxxxxxxxxxxxxxxxx
└────── Header ──────┘ └─────────────── Payload ───────────────────────┘ └─ Signature ─┘
3 つの部分
| 部分 | 内容 |
|---|---|
| Header | アルゴリズム (alg) と鍵 ID (kid) |
| Payload | claim (ユーザーの情報) |
| Signature | Header + Payload の暗号署名 (RS256 など) |
必須 claim (OpenID Connect Core 1.0 規定)
| Claim | 意味 |
|---|---|
iss (issuer) | 発行者 (例: https://accounts.google.com) |
sub (subject) | ユーザーの一意 ID (発行者内でユニーク) |
aud (audience) | このトークンの宛先 (Client ID) |
exp (expiration) | 有効期限 (Unix 時刻) |
iat (issued at) | 発行時刻 |
検証手順 (Client 側)
- 署名検証: 発行者の公開鍵 (
/.well-known/openid-configurationから取得) でSignatureを検証 issが想定の発行者かaudが自分の Client ID かexpが現在時刻より未来かnonce(認可リクエスト時に Client が送った 1 回限りのランダム値、リプレイ攻撃防止用) と一致するか
これらすべて OK のときだけ、payload の sub 等を信頼してユーザーを特定します。
ソーシャルログインの DB 設計
「Google でログイン」のような外部 IdP (Identity Provider) との連携を保存する DB 設計です。
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(100) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_oauth_providers (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(30) NOT NULL, -- 'google' / 'github' / 'apple'
provider_user_id VARCHAR(255) NOT NULL, -- IdP 内の sub (ユーザーID)
access_token TEXT, -- 暗号化推奨
refresh_token TEXT, -- 暗号化推奨
token_expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMPTZ,
PRIMARY KEY (provider, provider_user_id), -- 1 IdP の 1 ユーザーは 1 行
UNIQUE (user_id, provider) -- 1 ユーザーは 1 IdP につき 1 連携
);
CREATE INDEX idx_oauth_user ON user_oauth_providers(user_id);
ポイント:
- 主キーを
(provider, provider_user_id)の複合にすると「同じ Google アカウントで複数ユーザーを作る」を防げる access_token/refresh_tokenはアプリ層で暗号化してから保存 (鍵は別管理、KMS など)- 1 ユーザーが複数の IdP (Google + GitHub) を連携できる設計
トークンの保存と更新
OAuth 2.0 のトークンには 2 種類あります。
| トークン | 用途 | 寿命 |
|---|---|---|
| Access Token | API 呼び出し時に毎回送信 | 短命 (典型的に 1 時間、IdP によって異なる) |
| Refresh Token | Access Token が切れたら新しいものを取得 | 長命 (数日〜数ヶ月、IdP によって異なる) |
有効期限は OAuth 2.0 仕様 (RFC 6749) で定義されておらず、IdP の設定次第です。例:
- Google: Access Token 1 時間 / Refresh Token 期限なし (使われている限り)
- Facebook: Access Token 1-2 時間 / Long-lived Access Token 60 日
- Microsoft: Access Token 1 時間 / Refresh Token 90 日
「業界標準は X 時間」と決まった値はないので、IdP のドキュメントを確認してください。
Refresh Token を使った更新
-- アプリ層のロジック (Node.js 擬似コード)
async function getValidAccessToken(userId, provider) {
const row = await db.query(
'SELECT access_token, refresh_token, token_expires_at FROM user_oauth_providers WHERE user_id = $1 AND provider = $2',
[userId, provider]
);
// 5 分以内に期限切れなら refresh
if (row.token_expires_at <= new Date(Date.now() + 5 * 60 * 1000)) {
const newTokens = await refreshOauthToken(provider, row.refresh_token);
await db.query(
'UPDATE user_oauth_providers SET access_token = $1, refresh_token = $2, token_expires_at = $3 WHERE user_id = $4 AND provider = $5',
[encrypt(newTokens.access_token), encrypt(newTokens.refresh_token), newTokens.expires_at, userId, provider]
);
return newTokens.access_token;
}
return decrypt(row.access_token);
}
演習
次のシナリオに最適な OAuth フローを選んでください。
- ブラウザ上の SPA (React アプリ) で Google ログイン
- iOS ネイティブアプリで Spotify と連携
- サーバ間 (バックエンド ← API) で外部サービスを定期的に呼び出す
- 古いシステムで Implicit Flow を使っている
解答例
| 番号 | フロー | 理由 |
|---|---|---|
| 1. SPA で Google ログイン | Authorization Code + PKCE | SPA は Public Client、PKCE で認可コード盗難を防ぐ |
| 2. iOS ネイティブアプリ | Authorization Code + PKCE | ネイティブアプリも Public Client。SDK を使うのが現実的 |
| 3. サーバ間連携 | Client Credentials Flow | ユーザーの代理でなくサーバ自身のアクセス。Authorization Code は使わない |
| 4. 古い Implicit Flow | Authorization Code + PKCE に移行 | Implicit Flow は RFC 9700 で非推奨、移行計画を立てる |
補足: Client Credentials Flow はユーザーが介在しないサーバ間連携用。例: バックエンドが Slack API でメッセージを投稿する。
ありがちな間違い:
- 「JWT があれば認可サーバ不要」: JWT は単なるトークン形式で、認可サーバが発行する必要があるという基本は変わらない
- 「SPA なら Implicit Flow で OK」: 古い情報。SPA でも 2026 年現在は Authorization Code + PKCE が正解
- 「Client Credentials なら PKCE 不要」: Client Credentials Flow は認可コードが介在しないため PKCE 自体が適用外 (= 認可コード盗難リスクが存在しない)
あなたが Client で、Google が発行した ID Token を受け取りました。検証手順を 5 ステップで答えてください。
解答例
- トークンを 3 つに分割:
header.payload.signatureの形式で.で分割 - 公開鍵を取得:
https://accounts.google.com/.well-known/openid-configurationからjwks_uriを取得し、その JWKS からheader.kidに対応する公開鍵を取り出す - 署名を検証:
header.alg(典型的にRS256) でheader.payloadの署名を公開鍵で検証 - claim を検証:
issがhttps://accounts.google.comかaudが自分の Client ID かexpが現在時刻より未来か (exp > now)iatが現在時刻に近いか (abs(iat - now) < 5min程度のずれを許容)- (送信時に
nonceを含めたなら)nonceが一致するか
- OK ならユーザー特定:
payload.subを「Google での一意 ID」としてuser_oauth_providers.provider_user_idと照合し、紐づく自社ユーザーを特定
実装ライブラリ (Node.js):
import * as jose from 'jose';
const JWKS = jose.createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs'));
const { payload } = await jose.jwtVerify(idToken, JWKS, {
issuer: 'https://accounts.google.com',
audience: process.env.GOOGLE_CLIENT_ID,
});
// payload.sub が Google でのユーザー ID
const user = await findUserByOauth('google', payload.sub);
ありがちな間違い:
- 署名検証をスキップ: payload だけ base64 デコードして使うのは致命的。攻撃者が任意の payload を作れる
alg: noneを許可: 古い JWT ライブラリの脆弱性。署名なし JWT を受け付けると改ざんし放題- 公開鍵をハードコード: Google は定期的に鍵を回転する。JWKS から動的取得
まとめ
この章で学んだことを整理します。
- OAuth 2.0 は 認可 (アクセス権) のプロトコル、OIDC は OAuth 2.0 上の 認証 (本人確認) 拡張
- 現代の標準は Authorization Code Flow + PKCE (public client は RFC 9700 で必須、OAuth 2.1 では標準装備)
- Implicit Flow は非推奨・削除 された (古いチュートリアルに注意)
- ID Token は JWT (RFC 7519) 形式、必須 claim は
iss/sub/aud/exp/iat - JWT 検証は「署名 → claim → ユーザー特定」の順、JWT ライブラリを使う
- ソーシャルログインの DB は
(provider, provider_user_id)を主キーに、トークンは暗号化保存
本ガイド第 3 部 (応用編) はここまでです。次の章からは実践編に入り、総合題材で要件 → E-R 図 → 設計の一気通貫を扱います。