認証
更新日:
「誰が来たのかを確かめる仕組み」と、それを本番で安全に回し続ける運用のまとめ。攻撃手法ごとの対策は Webセキュリティ対策、フレームワーク別の実装は Next.js_08_認証とセキュリティ / TanStack Start_06_認証・DB・デプロイ・本番チェック を見る。
結論を先に書くと:
- 自作しない。認証サービスか実績あるライブラリを使い、自分は「設定」と「認可」に集中する
- ログイン状態は HttpOnly Cookie のセッションで持つのが基本
- 外部ログインは OAuth 2.0 + OpenID Connect(認可コードフロー + PKCE)
- これから作るなら パスキーを第一候補に、パスワードは補助
- 一番事故が多いのは認証ではなく認可(他人のデータに触れてしまう)
用語の整理
| 用語 | 意味 | 例 |
|---|---|---|
| 認証(Authentication / AuthN) | あなたは誰か | ログイン |
| 認可(Authorization / AuthZ) | あなたは何をしてよいか | 自分の注文だけ見られる、管理者だけ削除できる |
| セッション | ログイン状態をリクエストをまたいで覚えておくこと | Cookie に入ったセッションID |
| IdP(Identity Provider) | ログインを代行してくれる側 | Google、Apple、GitHub、Auth0 |
| MFA(多要素認証) | 種類の違う要素を2つ以上組み合わせる | パスワード + 認証アプリ |
本人確認の3要素
- 知識: 知っているもの(パスワード、PIN)
- 所持: 持っているもの(スマホ、セキュリティキー、パスキー)
- 生体: 本人の体(指紋、顔)
パスワード2つは「2段階」でも「2要素」ではない。違う種類を組み合わせて初めて多要素。
仕組み① ログイン状態の持ち方
HTTP は1回ごとに独立していて「さっきの人」を覚えていない(HTTP・HTTPS・TLS)。そこでログイン成功後に「通行証」を渡し、以降のリクエストで毎回見せてもらう。通行証の持ち方は大きく2つ。
セッション方式(サーバーが覚える)
1. ID/パスワードを送る
2. サーバーがDBにセッションを作り、ランダムなIDを Cookie で返す
Set-Cookie: session=8f3a...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=...
3. 以降ブラウザが自動で Cookie を付けて送る → サーバーがDBで照合トークン方式(トークン自体に情報を持たせる)
1. ログインすると、署名付きのトークン(JWT など)が返る
2. 以降 Authorization: Bearer <token> で送る
3. サーバーは署名を検証するだけ(DBを見なくてよい)| セッション(Cookie + DB) | トークン(JWT) | |
|---|---|---|
| 状態 | サーバーに保存 | トークンに埋め込む(ステートレス) |
| 即時ログアウト・強制失効 | 簡単(DBから消すだけ) | 難しい(期限まで有効。失効リストが要る) |
| スケール | セッションストアが必要 | 検証だけなので軽い |
| 向いている場面 | 普通のWebアプリ | API、モバイルアプリ、マイクロサービス間 |
実務の判断: ブラウザ向けWebアプリならセッション Cookieで十分かつ安全。JWT は「失効が難しい」弱点があるので、使うなら下記の短命トークン + リフレッシュトークン構成にする。
JWT を使うときの注意
- 中身は暗号化ではなく Base64。誰でも読める → 秘密情報を入れない
- 署名アルゴリズムをサーバー側で固定する(
alg: noneや鍵の取り違え攻撃を防ぐ) exp(期限)・iss(発行者)・aud(宛先)を必ず検証する- ブラウザの
localStorageに置くと XSS で盗まれる → ブラウザなら HttpOnly Cookie に入れる
アクセストークン + リフレッシュトークン
- アクセストークン: 短命(5〜15分程度)。API を叩くのに使う。盗まれても被害時間が短い
- リフレッシュトークン: 長命。アクセストークンの再発行にだけ使う。サーバー側で保存・失効できるようにする
- ローテーション: リフレッシュするたびに新しいリフレッシュトークンを発行し古いものは無効化。古いものが再利用されたら盗難とみなして一族ごと失効させる
仕組み② パスワード
自前でパスワードを扱う場合の最低ライン。
- 平文・暗号化で保存しない。ハッシュ化する(元に戻せない形)
- アルゴリズムは Argon2id(第一候補)か bcrypt。MD5・SHA-256 単体は速すぎて総当たりに弱いので不可
- ソルト(ユーザーごとのランダム値)はライブラリが自動で付ける
- ルールは NIST SP 800-63B の考え方が今の標準
- 長さを重視(最低8文字、できれば15文字以上を許容・推奨)。長いパスフレーズを拒否しない
- 「記号必須」などの複雑性ルールを強制しない、定期変更を強制しない(漏洩が疑われたときだけ変更)
- 漏洩済みパスワードのリストと照合して弾く(Have I Been Pwned の API など)
- パスワードマネージャーの貼り付けを妨げない
- ログイン失敗時のメッセージは「メールアドレスまたはパスワードが違います」で統一(どちらが違うか教えると、登録済みメールアドレスを調べられてしまう=ユーザー列挙)
仕組み③ OAuth 2.0 と OpenID Connect(「Googleでログイン」)
- OAuth 2.0: 本来は「権限を貸す」仕組み(例: このアプリに Google カレンダーの読み取りを許可する)。アクセストークンが発行される
- OpenID Connect(OIDC): OAuth の上に「誰がログインしたか」を乗せた仕様。ID トークン(JWT)で本人情報が返る
- 「〇〇でログイン」は認証なので OIDC。アクセストークンを「ログインの証拠」として扱うのは誤り
認可コードフロー + PKCE(今の標準)
1. アプリ → IdP へリダイレクト
(client_id, redirect_uri, scope=openid email, state, nonce, code_challenge)
2. ユーザーが IdP でログインし、許可する
3. IdP → redirect_uri に「認可コード」を付けて戻す
4. アプリのサーバーが、認可コード + code_verifier を IdP に送ってトークンと交換
5. ID トークンの署名・iss・aud・exp・nonce を検証 → 自分のサービスのセッションを作る- PKCE: 認可コードを横取りされても、元の
code_verifierを知らないと交換できない仕組み。モバイル・SPA だけでなく全クライアントで使うのが現在の推奨 state: CSRF 対策。行きと帰りで一致するか確認redirect_uriは完全一致で登録。ワイルドカードや前方一致にしない(トークン横取りの原因)- 古い Implicit フロー(トークンを URL で直接返す)は非推奨。使わない
- IdP 側の **
sub(ユーザーの固定ID)**で自分のユーザーと紐づける。メールアドレスは変わりうるので主キーにしない
仕組み④ パスキー(WebAuthn / FIDO2)
パスワードの代わりに公開鍵暗号でログインする方式。
登録: 端末が鍵ペアを作る → 公開鍵だけをサーバーに保存(秘密鍵は端末から出ない)
ログイン: サーバーがランダムな値(チャレンジ)を送る
→ 端末が指紋・顔・PINで本人確認し、秘密鍵で署名
→ サーバーが公開鍵で検証- フィッシングに強い: 鍵がドメインに紐づくので、偽サイトでは署名が作れない
- サーバーから漏れても困らない: 保存しているのは公開鍵だけ
- iCloud キーチェーン / Google パスワードマネージャーで端末間同期されるので、機種変更でも消えにくい
- 1つで「所持(端末)+ 生体 or 知識(ロック解除)」を満たすので、それ単体で多要素相当
- 運用上は、端末紛失に備えて複数のパスキー登録や別の復旧手段を用意する
仕組み⑤ その他のログイン方式
| 方式 | 仕組み | 注意 |
|---|---|---|
| マジックリンク / メールOTP | メールにリンクや6桁コードを送る | メールアカウントが乗っ取られると終わり。リンクは短命・1回限り |
| TOTP(認証アプリ) | 30秒ごとに変わる6桁コード(Google Authenticator 等) | MFA の定番。フィッシングサイトにコードを入力されると突破されうる |
| SMS コード | 電話番号にコード送信 | SIMスワップ等で弱い。他に手段がない場合の最終手段 |
| プッシュ承認 | スマホに「ログインしますか?」通知 | 通知を連打して誤承認させる攻撃(MFA疲労)→ 番号照合を併用 |
強さの目安: パスキー / セキュリティキー > TOTP > SMS > パスワードのみ
仕組み⑥ サーバー間・機械の認証
人ではなくプログラム同士の認証。
- APIキー: 一番シンプル。漏れたら即ローテーション → APIキー・シークレット管理
- OAuth クライアントクレデンシャルフロー: サービスが自分の client_id/secret で短命トークンを取得
- 署名付きリクエスト / Webhook 署名: 送信元が共有鍵で HMAC 署名し、受信側が検証(LINE・Stripe 等の Webhook)
- mTLS: クライアント側も証明書を出す。社内システムやゼロトラスト向け
- MCP のリモートサーバーも OAuth 2.1 ベースで認可する(MCP)
仕組み⑦ モバイルアプリ(iOS)で気をつけること
- トークンは Keychain に保存(UserDefaults は平文なので不可)
- 外部ログインは
ASWebAuthenticationSession(システムのブラウザ画面)を使う。アプリ内 WebView でパスワードを入力させない - App Store 審査の要件
- Google 等のサードパーティログインを提供するなら、同等のプライバシー配慮があるログイン手段(Sign in with Apple など)も提供する(Guideline 4.8)
- アカウント作成ができるアプリは、アプリ内からアカウント削除できるようにする(Guideline 5.1.1(v))
- ログインが必要なアプリは審査用のデモアカウントを App Review Information に記載する
- ログイン機能がないアプリは「サインイン情報: 不要」で済む。OS 標準の権限許可(MusicKit 等)はアプリ側の認証とは別物
実際の運用
1. 何を使うか決める
| 種類 | 例 | 向いている場合 |
|---|---|---|
| ホスト型サービス | Clerk、Auth0、WorkOS、Firebase Auth、Supabase Auth | 早く・安全に作りたい。MFA・パスキー・管理画面が最初から付く。ユーザー数に応じて課金 |
| ライブラリ(自分のDBに保存) | Better Auth、Auth.js | データを手元に置きたい、ランニングコストを抑えたい。運用責任は自分 |
| ゼロトラスト型の門番 | Cloudflare Access | 社内ツール・管理画面に「特定メールの人だけ」通したい。アプリ側の実装ほぼ不要(Cloudflare) |
| 自作 | — | 学習目的以外では選ばない |
選ぶときの観点: 料金(MAU 課金の上がり方)、パスキー・MFA 対応、使うフレームワークの公式サンプル有無、ユーザーデータをエクスポートして乗り換えられるか(ベンダーロックイン)、日本語メールテンプレート、障害時の影響。
2. 初期設定で決めておくこと
- セッション期限
- 一般サービス: アイドル期限(しばらく操作がないと切れる)+ 絶対期限(最長でも何日)の2本立て
- 管理画面・お金を扱う画面: 短め。重要操作(パスワード変更・送金・退会)の直前に再認証を求める
- Cookie:
HttpOnly/Secure/SameSite=Lax/ 適切なPathと期限。サブドメインと共有するかは意識して決める - ログイン後にセッションIDを作り直す(セッション固定攻撃対策。ライブラリは通常自動)
- リダイレクト先の制限:
?next=/dashboardは自サイト内パスのみ許可 - 環境ごとに IdP のクライアントを分ける(開発・ステージング・本番で redirect_uri もシークレットも別)
- メール送信: 確認メール・リセットメールの送信元ドメインに SPF / DKIM / DMARC を設定(迷惑メール行きと、なりすまし防止)
3. 日常的に回すこと
- レート制限: ログイン・パスワードリセット・OTP 送信・新規登録に回数制限。IP 単位とアカウント単位の両方で。Cloudflare の Rate Limiting / Turnstile が手軽
- アカウントを完全ロックすると嫌がらせに悪用されるので、段階的な待ち時間や CAPTCHA 追加が無難
- 監査ログ: ログイン成功/失敗、MFA 設定変更、パスワード・メール変更、権限変更、管理者操作を、日時・IP・端末と一緒に記録
- パスワード・トークン・OTP の値はログに出さない
- ユーザーへの通知: 新しい端末からのログイン、パスワード・メールアドレス変更時にメールで知らせる(乗っ取りに本人が気づける)
- **ログイン中の端末一覧と「他の端末からログアウト」**機能を提供する
- 依存ライブラリ・認証 SDK の更新: 認証まわりの脆弱性修正は優先して当てる
4. 状態が変わるときの処理
| イベント | やること |
|---|---|
| ログアウト | サーバー側のセッションを削除(Cookie を消すだけでは不十分) |
| パスワード変更・リセット | 他の全セッションとリフレッシュトークンを失効。完了通知メール |
| メールアドレス変更 | 新アドレスで確認してから反映し、旧アドレスにも通知 |
| 権限変更・アカウント停止 | 既存セッションにも即時反映(JWT だと期限まで残るので注意) |
| 退会 | セッション・トークン失効、IdP 連携解除、データ削除/匿名化のポリシーに従う |
5. アカウント復旧(一番弱くなりがちな入口)
どれだけログインを固くしても、「パスワードを忘れた」経路が弱ければそこから破られる。
- リセットリンクは短命(15〜60分)・1回限り・推測不能なトークン。DB にはハッシュ化して保存
- 「メールアドレスが登録されていれば送信しました」と、登録有無で応答を変えない
- MFA を設定しているユーザーは、リセット後も MFA を要求する
- MFA 紛失に備えてリカバリーコードを最初に発行し、保管を促す
- サポート窓口での本人確認手順を決めておく(電話やチャットでの「なりすまし」によるリセットはよくある手口)
6. 鍵・シークレットのローテーション
- JWT 署名鍵:
kid(鍵ID)付きで JWKS として公開し、新鍵を追加 → 新鍵で署名開始 → 旧鍵で署名されたトークンが期限切れになったら旧鍵を削除、の順で無停止で切り替え - OAuth のクライアントシークレット・セッション暗号化キー・Webhook 署名鍵: 定期的に、かつ漏洩の疑いがあれば即時に交換。新旧を一時的に両方受け付けられる作りにしておくと安全
- 置き場所は APIキー・シークレット管理 / Cloudflare_04_環境変数・シークレット・環境分離 のとおり。リポジトリや
NEXT_PUBLIC_には絶対に置かない
7. 障害・インシデント時
- IdP / 認証サービスが落ちた場合: 新規ログインはできなくなる。既存セッションで使い続けられるか、ステータスページをどこで確認するかを把握しておく
- 漏洩・乗っ取りが疑われる場合の手順を決めておく
- 影響範囲の特定(監査ログから)
- 該当ユーザー(または全員)のセッション・トークンを一括失効
- 必要ならパスワード強制リセット、関連シークレットのローテーション
- ユーザーへの通知(個人情報が絡む場合は法令上の報告義務も確認)
- 原因の修正と再発防止をログに残す
チェックリスト
実装
- 認証はサービス/実績あるライブラリを使っている(自作していない)
- セッションCookieに
HttpOnly/Secure/SameSite - パスワードは Argon2id / bcrypt でハッシュ化、漏洩パスワードを拒否
- OAuth/OIDC は認可コードフロー + PKCE、
state・nonce検証、redirect_uri 完全一致 - ID トークン/JWT の署名・
iss・aud・expを検証、アルゴリズム固定 - すべての API でサーバー側の認可チェック(本人のデータか)
- ログイン失敗・パスワードリセットで登録有無が分からない
- パスキーまたは MFA を提供している(管理者は必須)
運用
- ログイン・リセット・OTP にレート制限
- 認証イベントの監査ログ(秘密値は出力しない)
- ログアウト・パスワード変更で全セッション失効
- 新端末ログイン・重要変更時の通知メール
- リセットトークンは短命・1回限り・ハッシュ保存
- リカバリーコードと MFA 紛失時の手順がある
- 署名鍵・シークレットのローテーション手順がある
- 環境ごとに IdP クライアントとシークレットを分離
- 漏洩時の一括失効の手順を試したことがある
- (iOS)Sign in with Apple・アカウント削除・審査用デモアカウント
参考
- NIST SP 800-63B: パスワード・MFA の考え方の国際的な基準
- OWASP Authentication / Session Management Cheat Sheet: 実装時の具体的な注意点
- OAuth 2.0 Security Best Current Practice(RFC 9700): OAuth の安全な使い方
- passkeys.dev: パスキーの実装ガイド