新しいWebアプリの認証戦略の選び方
2026年のWebアプリ認証は、セッションかJWT、OAuth 2.0/OIDC、パスキー、管理型プロバイダを比較し、失効・CSRF・UXまで整理します。
2026年に新しいWebアプリを構築する場合、認証戦略はアプリの形態に応じて決まります。サーバーレンダリングのモノリスであれば、署名済みのHttpOnlyクッキーによるサーバーサイドセッションから始めるべきです。SPA、モバイルクライアント、またはAPIバックエンドであれば、HttpOnlyクッキーに保存したリフレッシュトークンのローテーションと組み合わせた短命なアクセストークンを使用します。ソーシャルログインやエンタープライズSSOが必要な場合は、OAuth 2.0/OIDCを上位レイヤーとして追加します。そして、認証自体がプロダクトのコア機能でない場合、最も堅実なデフォルト選択はマネージドプロバイダーです。どのベースを選んでもパスキーをフィッシング耐性のある認証要素として追加してください。本記事の残りの部分では、各選択肢を具体的な基準(失効、スケーリング、CSRF/XSSへの露出、複雑性、そして本番環境でのみ顕在化するUX上の障害モード)に照らし合わせて検証します。
多くの新しいアプリが陥る罠は、チュートリアルで使われていたからという理由だけでJWTを採用してしまうことです。ステートレスなスケーリングの必要性がなければ、失効コストを正当化できません。これはデフォルトの選択ではなく、比較検討すべき意思決定です。以下では判断基準、比較表、そしてアプリのアーキタイプから推奨策へのマトリクスを示します。
重要なポイント
- セッションとトークンの違いは、認証状態がどこに存在するか(サーバー側かクライアント側か)という問題であり、クッキーと
Authorizationヘッダーの違いは、クレデンシャルがどのように伝送されるかという問題に過ぎません。この2つを混同することが、不要なJWTをカーゴカルト的に採用してしまう最も一般的な原因です。 - JWTを
localStorageに保存することは現在アンチパターンです。ページ上のXSSがトークンを読み取って外部に送信できるため、クレデンシャルはJavaScriptがアクセスできないHttpOnlyクッキーに保存してください。 - JWTの難しい問題はトークンの発行ではなく、失効です。即時ログアウトを実現するには、短いトークン有効期限とリフレッシュトークンのローテーション、またはサーバーサイドの拒否リストが必要ですが、これによりJWTが排除しようとしていた状態管理が静かに再導入されます。
- OAuth 2.1はまだIETFのInternet-Draft(draft-15、2026年3月)であり、批准されたRFCではありませんが、その方向性は確定しています。必須PKCE、厳密なリダイレクトURIマッチング、ImplicitグラントおよびResource Owner Password Credentialsグラントの廃止が主な変更点です。
- 認証がプロダクトのコア機能でない場合、マネージドプロバイダー(Auth0、Clerk、Supabase Auth、WorkOS、Stytch)が2026年における現実的なデフォルト選択です。セッション、ソーシャルログイン、MFA、パスキーを、セキュリティ侵害リスクを自社で抱えることなく利用できます。
本質を捉えるフレームワーク:状態の保存場所と伝送方法
セッションとトークンの違いは、認証状態がどこに存在するか(サーバー側か、クライアントのクレデンシャル内か)という問題です。一方、クッキーとAuthorizationヘッダーの違いは、そのクレデンシャルがHTTP上でどのように伝送されるかという問題に過ぎません。これらは直交する2つの軸であり、1つの意思決定として扱うことが、多くのチームが不要なJWTを採用してしまう原因です。この区別は以降の説明の核心であり、認証ベンダーのAuthgearも同様の見解を示しています。
セッションはステートフルです。サーバーが正規のレコードを保持し、クライアントにはそれを参照するための不透明なIDを渡します。ホテルのキーカードモデルと同じで、フロントデスクがマスターリストを管理します。JWTはステートレスです。クレームが自己完結型で署名されているため、どのサーバーもルックアップなしに検証できます。デジタルパスポートモデルと同じで、どの係官も発行機関に問い合わせることなく署名を信頼します。どちらもクッキーでもAuthorizationヘッダーでも伝送できます。クッキーとヘッダーはトランスポートであり、セッションとトークンはアーキテクチャです。
| 基準 | サーバーサイドセッション | JWT / 自己完結型トークン |
|---|---|---|
| 状態の保存場所 | サーバー(ストア) | クライアント(署名済みトークン) |
| 失効 | 即時(レコードを削除するだけ) | 困難(短いTTL+ローテーション、または拒否リスト) |
| スケーリング | 共有ストアが必要(例:Redis) | ステートレス;リクエストごとのルックアップ不要 |
| クレデンシャルサイズ | 約32バイトの不透明なID | リクエストごとに800バイト以上のクレームが多い |
| 主な攻撃対象 | CSRF(クッキーは自動送信される) | XSS(JS読み取り可能なストレージからのトークン窃取) |
| 最適な用途 | サーバーレンダリングのモノリス | APIファースト、SPA、モバイル、マイクロサービス |
サイズの違いは、ステートレスとステートフルのトレードオフを端的に示しています。セッションクッキーは約32バイトの不透明なIDを運び、サーバーのルックアップが必要です。一方JWTはクレームをインラインで運び(多くの場合リクエストごとに800バイト以上)、ルックアップを省略します。ストアの排除と引き換えに、帯域幅と失効レイテンシを負担することになります。
セッション:サーバーレンダリングアプリのデフォルト選択
Discover how at OpenReplay.com.
サーバーレンダリングのモノリスには、署名済みのHttpOnly、Secure、SameSiteクッキーによるサーバーサイドセッションから始めてください。これが最もシンプルで正しい選択肢であり、即時失効を無償で実現できます。ブラウザはクッキーを自動的に送信し、JavaScriptに認証状態を保存する必要がなく、ユーザーのログアウトはサーバー側で1回削除するだけで完了します。
仕組みは4つのステップです。ユーザーがクレデンシャルを送信し、サーバーがセッションレコードを作成してそのIDをクッキーで返し、ブラウザがすべてのリクエストにそのクッキーを付与し、ログアウト時に両側を破棄します。パスワードはストアに保存する前にArgon2でハッシュ化してください。OWASPのPassword Storage Cheat Sheetでは、Argon2を現在の第一選択アルゴリズムとして位置づけています。セッションIDは高エントロピーで生成し、機密データを含めないようにしてください。IDの長さ、ローテーション、タイムアウトについてはOWASPのSession Management Cheat Sheetが標準的なリファレンスです。
# FastAPI: issue an opaque session, store it server-side, set a hardened cookie
import secrets
from fastapi import FastAPI, Response
# redis client + Argon2 password verification omitted for brevity
app = FastAPI()
SESSION_TTL = 60 * 60 * 24 # 24 hours
@app.post("/login")
async def login(response: Response): # verify Argon2 hash first
session_id = secrets.token_urlsafe(32) # ~256 bits of entropy
await redis.setex(f"sess:{session_id}", SESSION_TTL, user_id)
response.set_cookie(
"session", session_id,
httponly=True, # JS cannot read it -> blunts XSS exfiltration
secure=True, # HTTPS only
samesite="lax", # mitigates CSRF on top-level navigations
max_age=SESSION_TTL,
)
return {"ok": True}
このコードスニペットはセッションの本質を示しています。シークレットはJavaScriptに存在せず、サーバーが真実の情報源を保持します。2つのトレードオフが生じます。第一にスケーリング:レコードがサーバーサイドにあるため、マルチインスタンス構成ではスティッキーセッション、またはより良い方法としてRedisのような共有ストアが必要になります。これにより任意のインスタンスが任意のセッションを解決できます。第二にCSRF:クッキーはクロスサイトリクエストで自動送信されるため、SameSiteと組み合わせて定数時間比較によるCSRFトークンを使用してください。詳細はOWASPのCSRF Cheat Sheetを参照してください。HttpOnlyフラグはXSSによる読み取りを防ぎ、SameSiteとトークンの組み合わせがCSRFを防ぎます。
JWTとトークン:失効コストを伴うステートレスなスケーリング
JWTを直接使用するのは、具体的なステートレススケーリングの理由がある場合に限ってください。複数のクライアントタイプにサービスを提供するAPI、サービス間でアイデンティティを受け渡すマイクロサービス、またはクッキーが扱いにくいクロスドメインアクセスなどが該当します。デフォルトの選択として使うべきではありません。メリットは確かにあります。署名済みトークンは公開鍵に対してローカルで検証されるため、リクエストごとのストアルックアップをスキップでき、どのノードでも任意のリクエストを認証できます。
JWTの難しい問題はトークンの発行ではなく、失効です。トークンが自己完結型であるため、サーバー側の状態に関わらず有効期限が切れるまで有効であり続けます。即時ログアウトを実現するには、短いトークン有効期限とリフレッシュトークンのローテーション、またはすべてのリクエストで確認するサーバーサイドの拒否リストが必要です。そしてこの拒否リストは、JWTが排除しようとしていた状態管理を静かに再導入することになります。標準的なパターンは、短命なアクセストークンと、より長命でローテーションされるリフレッシュトークンの組み合わせです。OAuth 2.0 Security Best Current Practice(RFC 9700、2025年1月)は、リフレッシュトークンのローテーションと短いアクセストークン有効期限に関する批准済みの権威ある参照資料であり、ブログ記事に散見されるキリの良いTTL値よりも信頼できる情報源です。
トークンの保存場所が、侵害時の影響範囲を決定します。JWTをlocalStorageに保存することは現在アンチパターンです。ページ上のXSSがトークンを読み取って外部に送信できるため、JavaScriptがアクセスできないHttpOnlyクッキーにトークンを保存してください。ブラウザアプリに関しては、IETFのOAuth for Browser-Based Appsのベストカレントプラクティスドラフトが、PKCEを使用したAuthorization Codeフローを推奨しており、悪意のあるJavaScriptはトークンを読み取る以上のことができると警告しています。
# FastAPI: verify an access token with PyJWT (not python-jose)
import jwt # PyJWT
from datetime import datetime, timezone
from fastapi import HTTPException
def verify_access_token(token: str, public_key: str) -> dict:
try:
return jwt.decode(
token, public_key,
algorithms=["RS256"], # never accept "none"; pin the alg
options={"require": ["exp", "iat", "sub"]},
)
except jwt.ExpiredSignatureError:
raise HTTPException(401, "token expired") # client should refresh
except jwt.InvalidTokenError:
raise HTTPException(401, "invalid token")
# When you mint tokens, use timezone-aware UTC:
# exp = datetime.now(timezone.utc) + timedelta(minutes=15)
最新の状況を把握するための2つのバージョンに関する注意点があります。PyJWTを使用してください。FastAPIの公式セキュリティチュートリアルはpwdlibとともにPyJWT(import jwt)に移行しており、python-joseは現在最小限のメンテナンスしか行われていません。また、有効期限の生成にはdatetime.now(timezone.utc)を使用してください。datetime.utcnow()はPython 3.12で非推奨となり、Pythonの将来の削除予定に関するドキュメントによれば、削除バージョンは未定のまま非推奨状態が続いています。現在も動作しますが、タイムゾーン対応の呼び出しが推奨される代替手段です。
OAuth 2.0/2.1とOIDC:必要な場合の委任アイデンティティ
既存のアイデンティティプロバイダー(「Googleでサインイン」、GitHub、Microsoft、Oktaなど)でユーザーにサインインさせたい場合、またはエンタープライズSSOが必要な場合は、OpenID ConnectとともにOAuth 2.0を使用してください。これはVIPゲストリストモデルです。ユーザーを自分で検証するのではなく、すでにそのユーザーを知っている発行者を信頼します。OAuth 2.0は委任された認可(APIへのアクセストークン)を処理し、OpenID Connectがその上にアイデンティティトークンを重ねることで、誰がユーザーであるかを把握できます。
委任またはSSOが真の要件である場合にのみ、この複雑さに見合う価値があります。なぜなら、このフローには認可コード、PKCE、スコープ、リダイレクトURI、JWKSキーローテーションといった実際の可動部品が伴うからです。ソーシャルログイン要件のないファーストパーティログインのみのMVPでは、OAuthは過剰です。委任のために存在するリダイレクト機構を抱えながら、その委任機能を使わないことになります。
採用する場合は、モダンなプロファイルをターゲットにしてください。OAuth 2.1はまだIETFのInternet-Draft(draft-15、2026年3月2日公開)であり、批准されたRFCではありませんが、その方向性は確定しています。OAuth 2.0からの主な変更点は、Authorization Codeフローを使用するすべてのクライアントでPKCEが必須となること、リダイレクトURIは厳密な文字列マッチングで比較されること、ImplicitグラントとResource Owner Password Credentialsグラントが削除されることです。IETFはInternet-Draftsの有効期間は最長6か月であり、「作業中」以外の形で引用することは不適切であると注記しています。そのため、OAuth 2.1は出荷済みの標準ではなく、主要なアイデンティティプロバイダーによってすでに実践で適用されている目標とすべき統合仕様として扱ってください。本質的な内容が重要です。機密性の高いサーバーサイドのクライアントを含むすべてのクライアントにAuthorization Code + PKCEを使用し、ワイルドカードのリダイレクトURIを廃止してください。
パスキーとWebAuthn:フィッシング耐性を持つ進化の方向性
パスキーは宗教的な信念としてではなく、フィッシング耐性のある認証要素として追加してください。WebAuthn/FIDO2に基づくパスキーは公開鍵/秘密鍵のペアであり、秘密鍵はデバイスのセキュアハードウェアに保存され、ローカルの生体認証またはPINチェック後にサーバーチャレンジに署名します。クレデンシャルはオリジンにバインドされており、送信されることがないため、パスワードやSMSコードでは不可能な方法でフィッシング、リプレイ攻撃、クレデンシャルスタッフィングに対する耐性を持ちます。
政策面での変化は決定的です。NCSCは2026年4月に、パスキーはすべてのデジタルサービスにおいて消費者の第一選択のログイン方法となるべきであると発表し、サービスがサポートしている場合はパスキーを、そうでない場合は2段階認証を推奨し始めると表明しました。これは単一の突然の変更ではなく、ガイダンスの継続的な更新を通じて反映される変化です。サポートは事実上ユニバーサルです。パスキーはWindows、ChromeOS、macOS、Android、iOS、多くのLinuxディストリビューションなどの主要なオペレーティングシステム、およびChrome、Firefox、Edge、Safariなどのブラウザでサポートされています。
パスキーのサポートは解決済みですが、パスキーの登録はまだ課題があります。これが実際の選択制約です。Corbadoのパスキーベンチマーク2026(ベンダーによるベンチマークであり、中立的な統計ではありません)によれば、Webでの初回登録成功率はiOSで約49〜83%ですが、Windowsでは25〜39%に留まります。登録UXとアカウントリカバリーを第一級の設計課題として扱ってください。パスキーはオプトインのアップグレードとして明確なフォールバックとともに提供し、リカバリーはリリース前に設計してください。デバイスを紛失してリカバリー手段がないユーザーはロックアウトされてしまいます。
Auth-as-a-Service:認証がプロダクトのコア機能でない場合の現実的なデフォルト
認証がプロダクトのコア機能でない場合、2026年における最も堅実なデフォルト選択はマネージドプロバイダーです。Auth0、Clerk、Supabase Auth、WorkOS、またはStytchを使用することで、セキュリティ侵害リスクやメンテナンスを自社で抱えることなく、セッション、ソーシャルログイン、MFA、パスキーを利用できます。これらのベンダーはドロップインSDKを提供しており、OAuthリダイレクト機構やWebAuthnのセレモニーを含む完全なライフサイクルを処理します。これらは自前で構築してデバッグしなければならないものです。
この判断は純粋に経済的なものです。自前で構築すれば制御を得られ、MAUあたりのコストはゼロです。マネージドプロバイダーを使えば、堅牢な実装、ログイン機能の迅速な提供、そしてセキュリティクリティカルなパスのオンコールローテーションを他社に委ねられます。ログインが差別化要因ではなく当然の機能である多くのグリーンフィールドアプリにとって、マネージドプロバイダーは合理的な出発点です。スケールやコストが要求する場合は、後から移行することもできます。
認証方法の意思決定マトリクス
戦略はアプリのアーキタイプに従います。自分のアプリの形態を該当する行にマッピングし、そこから始め、フィッシング耐性のある要素としてパスキーを全体的に追加してください。
| アプリのアーキタイプ | 出発点 | 必要に応じて追加 |
|---|---|---|
| サーバーレンダリングのモノリス | サーバーサイドセッション(HttpOnlyクッキー) | ソーシャルログイン用OIDC;パスキーのオプトイン |
| SPA / モバイル / APIバックエンド | 短命なアクセストークン+リフレッシュローテーション(HttpOnlyクッキー内) | サードパーティアイデンティティ用OIDC |
| ソーシャルログインまたはエンタープライズSSOが必要 | OAuth 2.0 + OIDC(2.1プロファイルをターゲットに) | 自社サーフェス用のセッションまたはトークン |
| 認証がコア機能でない | マネージドプロバイダー(Auth0/Clerk/Supabase/WorkOS/Stytch) | — 残りはすでにバンドル済み |
| 高セキュリティ / 消費者向け | 上記のいずれかのベース + パスキー | 事前に設計されたリカバリーフロー |
本番環境での一般的なアーキテクチャはハイブリッドです。サーバーレンダリングのWebアプリにはセッション、SPAとモバイルが利用するAPIにはJWT、ソーシャルサインインにはOIDC、そしてオプトインのアップグレードとしてパスキーを組み合わせます。これらすべての根底にある業界のトレンドは、パスワードの送信回数を減らし、送信場所を限定し、より多くのスコープと説明責任を持たせることです。これはまさに、スコープ付きトークンがパスワードベースのGit認証を置き換えた理由であり、パスキーが今やパスワードを完全に置き換えようとしている理由でもあります。
セッションリプレイが明らかにする認証UXの障害モード
セッションリプレイは実際のクライアントサイドのフローを再現する技術であり、認証ジャーニーを再生することで、バックエンドログでは見逃される典型的な障害モードのカタログが浮かび上がります。紙の上で正しい戦略を選んでも、ユーザーが実際にログインできるとは限らないからです(これらはこの技術の能力であり、測定されたインシデント発生率ではありません)。
- サイレントなトークン有効期限切れによるログアウトループ。 リフレッシュトークンがセッション中に期限切れになると、SPAがユーザーを繰り返しログインページにリダイレクトすることがあります。リプレイはタイムライン上でリダイレクトループを示しますが、個々のリクエストが技術的に有効な401を返すため、エラーログではこれを見逃します。
- パスキー登録の離脱。 リプレイは、ユーザーがWebAuthnのセレモニーのどこで離脱するかを示します。OSのプロンプトが表示されてユーザーがキャンセルする場合がこれに当たり、Windowsでの登録率の低さが予測する正確な障害です。
- OAuthリダイレクトの行き詰まり。 同意後のリダイレクトが空白またはエラーページに着地する場合(リダイレクトURIの不一致、
stateパラメータの喪失)、ラウンドトリップがサーバー外で発生するため、バックエンドログには記録されません。リプレイはユーザーが見たとおりの行き詰まりを捉えます。 - ブラウザ固有のクッキー障害。 ローカルでは再現しない
SameSiteやSecureクッキーの問題は、リプレイが失敗したセッションを特定のブラウザとバージョンに関連付けることで明らかになります。
重要なのは、認証はプロトコルの選択だけでなくUXの問題でもあるということです。選択した戦略によって、どの障害クラスをデバッグすることになるかが決まります。
アプリの形態に合ったベースを選び、クレデンシャルストレージを堅牢にし(HttpOnlyクッキー、localStorageは絶対に使わない)、フィッシング耐性のある要素としてパスキーをオプトインで追加してください。多くの新しいアプリにとって、正直な最初の一手はマネージドプロバイダーまたはセッションファーストのモノリスです。実際のステートレススケーリング要件が失効コストを正当化する場合にのみ、生のJWTを選択してください。ログイン機能がリリースされたら、実際のフローを監視してください。健全な戦略と機能する戦略の差は、本番環境でのみ現れるUXの細部にあります。
よくある質問
新しいWebアプリにはデフォルトでJWTを使うべきですか?
いいえ。生のJWTは、複数のクライアントタイプにサービスを提供するAPIやサービス間でアイデンティティを受け渡すマイクロサービスなど、具体的なステートレススケーリングの理由がある場合にのみ使用してください。サーバーレンダリングのモノリスには、サーバーサイドセッションの方がシンプルで即時失効を実現できます。JWTは自己完結型であるため、有効期限前に失効させるには、短いトークン有効期限とリフレッシュトークンのローテーション、またはサーバーサイドの拒否リストが必要です。これによりJWTが排除しようとしていた正確なサーバー状態が再導入されます。
セッショントークンとJWTは同じですか?
いいえ。セッショントークンは約32バイトの不透明なIDであり、サーバーに保存された認証状態を参照するため、すべてのリクエストでルックアップが必要です。JWTはクレームをインラインで運び、ルックアップなしに署名に対してローカルで検証されます(リクエストごとに800バイト以上になることが多い)。この2つはステートレスとステートフルのトレードオフを表しています。セッションはサーバーに状態を保持し、JWTはクライアントの署名済みクレデンシャルに状態を移します。
入力をサニタイズすればJWTをlocalStorageに保存しても大丈夫ですか?
いいえ。JWTをlocalStorageに保存することは、入力のサニタイズに関わらずアンチパターンです。ページ上のどこかにクロスサイトスクリプティングの脆弱性があれば、localStorageを読み取ってトークンを外部に送信できるからです。JavaScriptがアクセスできないHttpOnlyクッキーにクレデンシャルを保存し、SameSiteとCSRFトークンと組み合わせてください。ブラウザアプリについては、IETFのOAuth for Browser-Based Appsのベストカレントプラクティスドラフトが、クライアントが読み取れるトークンストレージではなく、PKCEを使用したAuthorization Codeフローを推奨しています。
ファーストパーティログインのみのMVPにOAuthは過剰ですか?
はい。MVPがソーシャルサインインやエンタープライズSSOなしのファーストパーティログインのみを必要とする場合、OAuth 2.0は使用しない機構を追加することになります。このフローには認可コード、PKCE、スコープ、リダイレクトURI、JWKSキーローテーションが含まれており、これらはすべて委任アイデンティティをサポートするために存在します。「Googleでサインイン」、GitHub、またはエンタープライズSSOが真の要件である場合にのみ、OpenID ConnectとともにOAuthを採用してください。そうでなければ、セッションまたはマネージドプロバイダーから始めてください。