Webアプリの認証機能を実装する
パスワード認証を題材に、登録、ログイン、セッション、ログアウト、パスワード再設定を安全につなぐ実装手順。
概要
認証機能は、ログイン画面だけでは完結しません。
アカウント登録、パスワード保存、ログイン試行の制御、セッション発行、ログアウト、パスワード再設定までを一つの流れとして設計する必要があります。認証後の操作では、利用者がその処理を実行してよいかを確認する認可も必要です。
このページでは、ブラウザから利用するWebアプリにパスワード認証を実装する場合を中心に、処理のつながりと確認点を整理します。特定のフレームワークには依存しません。
実装前に決めること
コードを書く前に、少なくとも次の項目を決めます。
- 誰がアカウントを作成するか
- ログインIDにメールアドレス、社員番号、ユーザー名のどれを使うか
- メールアドレスの確認が必要か
- パスワード認証だけでよいか、MFAや外部IDプロバイダが必要か
- セッションの有効期間と、一定時間操作がない場合の期限
- 一台だけでなく、すべての端末からログアウトする機能が必要か
- パスワードを忘れた場合の本人確認をどうするか
社内システムでは、既存のID基盤やSSOを利用できる場合があります。認証を一から作る前に、組織の標準方式やフレームワークの認証機能を確認します。
全体の構成
パスワード認証とサーバー側セッションを使う場合、構成は次のようになります。
ブラウザ
├─ 登録・ログイン要求 ──> 認証処理 ──> ユーザー情報
└─ Session ID Cookie ──> セッション検証 ──> セッション情報
ブラウザには推測されにくいSession IDだけをCookieで持たせ、ログイン中のユーザーIDや有効期限はサーバー側で管理します。
最小限のデータ設計
users
| 項目 | 内容 |
|---|---|
id | 内部で使うユーザー識別子 |
login_id | ログインに使う値。重複を禁止する |
password_hash | パスワード用アルゴリズムで生成したハッシュ |
status | 有効、停止、退会などの状態 |
email_verified_at | メール確認が必要な場合の確認日時 |
ログインIDは入力時と検索時に同じ規則で正規化します。重複はアプリケーションの確認だけに頼らず、データベースの一意制約でも防ぎます。
sessions
| 項目 | 内容 |
|---|---|
id | セッションを識別する値 |
user_id | ログイン中のユーザー |
created_at | 発行日時 |
expires_at | 絶対的な有効期限 |
last_seen_at | 無操作期限を管理する場合の最終利用日時 |
revoked_at | ログアウトなどで無効化した日時 |
password_reset_tokens
| 項目 | 内容 |
|---|---|
token_hash | 再設定用トークンのハッシュ |
user_id | 対象ユーザー |
expires_at | 有効期限 |
used_at | 使用済み日時 |
再設定用トークンは十分にランダムな値を生成し、短い有効期限を設定します。データベースには、可能であればトークンそのものではなくハッシュを保存します。
アカウント登録
登録処理の基本的な流れは次のとおりです。
- ログインIDとパスワードを受け取る
- ログインIDを正規化し、形式と重複を確認する
- パスワードポリシーを確認する
- パスワード用のアルゴリズムでハッシュ化する
- ユーザーを保存する
- 必要であれば、メールアドレス確認用のトークンを発行する
パスワードは平文でも、復号できる暗号文でも保存しません。Argon2id、scrypt、bcrypt、PBKDF2など、利用する環境で推奨されるパスワード保存用の実装を使います。saltの生成や比較処理も、十分に利用されているライブラリやフレームワークに任せます。
パスワードの規則は、文字種を増やすことだけに寄せず、十分な長さ、よく使われるパスワードの拒否、試行回数の制御と組み合わせます。入力されたパスワードを黙って途中で切り捨ててはいけません。
ログイン
ログイン処理は、次の順序で考えます。
- HTTPSでログインIDとパスワードを受け取る
- ログインIDを登録時と同じ規則で正規化する
- 対象ユーザーを検索する
- 保存済みハッシュと入力されたパスワードを検証する
- アカウント状態を確認する
- 新しいセッションを発行する
- Session IDをCookieに設定する
ユーザーが存在しない場合とパスワードが違う場合で、「そのメールアドレスは登録されていません」のように応答を分けると、アカウントの存在を推測されます。画面には「ログインIDまたはパスワードが正しくありません」のような共通メッセージを返します。
ユーザーが存在しない場合だけ処理時間が極端に短くならないよう、ダミーのパスワードハッシュを検証する方法もあります。画面上の文言だけでなく、応答時間から情報が漏れないかも確認します。
ログイン試行を制限する
ログイン回数を無制限にすると、総当たり攻撃や、漏えいしたパスワードを別サービスで試す攻撃を受けやすくなります。
- アカウント単位と送信元単位の両方で回数を制限する
- 短時間の連続失敗には待ち時間を設ける
- 失敗回数だけで永久にロックしない
- 大量の失敗や不自然な地域からの試行を記録・検知する
- 管理者など影響の大きいアカウントにはMFAを検討する
送信元IPだけで制限すると、共有回線の利用者をまとめて止めたり、攻撃者にIPを切り替えられたりします。反対にアカウントだけで制限すると、第三者が意図的に対象アカウントをロックできます。複数の情報を組み合わせて段階的に制限します。
セッションを発行する
認証成功後は、ログイン前のSession IDをそのまま使わず、新しいSession IDを発行します。これにより、攻撃者が事前に用意したセッションを利用者に使わせるセッション固定攻撃を防ぎます。
Cookieの設定例は次のとおりです。
Set-Cookie: __Host-session=<推測困難な値>; Path=/; Secure; HttpOnly; SameSite=Lax
Secure: HTTPS通信でだけCookieを送るHttpOnly: JavaScriptからCookieを読み取りにくくするSameSite: 別サイトからのリクエストにCookieを送る範囲を制御するPath=/: アプリ全体で利用する__Host-:Secure、Path=/、Domainなしという厳しい条件をブラウザに要求する
SameSite=LaxかStrictかは、外部サイトから戻る導線やSSOの有無も含めて決めます。SameSite属性だけに任せず、状態を変更するリクエストではCSRFトークンも検証します。
Session IDをURL、HTML、アクセスログに含めません。セッションには無操作時の期限と絶対的な期限を設け、期限切れや無効化済みのセッションは拒否します。
認証後は毎回認可する
認証できたことは、すべての処理を実行してよいことを意味しません。
各リクエストでは、Session IDからユーザーを特定した後、そのユーザーが対象データを閲覧・更新できるかをサーバー側で確認します。ボタンを非表示にするだけでは認可になりません。URLやリクエストを直接送られても拒否できる必要があります。
ログアウト
ログアウトでは、画面をログインページへ移動するだけでは不十分です。
- サーバー側のセッションを無効化する
- ブラウザのSession Cookieを期限切れにする
- ログアウト後のSession IDでアクセスできないことを確認する
必要に応じて「この端末だけ」と「すべての端末」のログアウトを分けます。ログアウトを状態変更として扱い、POSTとCSRF対策を使う設計も検討します。
パスワード再設定
パスワード再設定は、ログインとは別の認証経路になります。ここが弱いと、強いパスワードを設定しても回避されます。
- ログインIDまたはメールアドレスを受け取る
- アカウントの有無にかかわらず同じ応答を返す
- 対象が存在する場合だけ、十分にランダムな一回限りのトークンを発行する
- HTTPSの再設定URLを本人が確認できる経路へ送る
- 有効期限、未使用、対象ユーザーを検証する
- 新しいパスワードを保存し、トークンを使用済みにする
- 完了通知を送り、既存セッションを失効させるか方針に従って処理する
秘密の質問だけを本人確認に使う方法は、答えを推測・調査されやすいため避けます。再設定トークンをログへ出したり、使用後も再利用できる状態にしたりしてはいけません。
セッションとJWTの選び方
ブラウザ中心の一般的なWebアプリでは、まずサーバー側セッションを検討します。サーバーで状態を無効化でき、ログアウトや権限変更を反映しやすいためです。
JWTは、複数サービス間で検証するAPIなどで役立つことがあります。ただし、JWTに変えただけで認証が安全になるわけではありません。保存場所、漏えい時の失効、短い有効期限、鍵の管理を別途設計する必要があります。要件がない段階で、ログイン機能だからJWTを選ぶ必要はありません。
ログに残すもの、残さないもの
認証の成功・失敗、アカウント停止、再設定要求、セッション失効などは、調査できる形で記録します。ただし、次の値はログへ残しません。
- パスワード
- Session ID
- パスワード再設定用トークン
- MFAの確認コード
- Authorizationヘッダーの認証情報
利用者を追跡するための内部IDやリクエストIDを使い、秘密情報を含めずに処理をたどれるようにします。
テスト観点
| 観点 | 確認内容 |
|---|---|
| 正常ログイン | 正しい情報で新しいセッションが発行される |
| 認証失敗 | 未登録IDと誤ったパスワードで同じメッセージになる |
| 試行制限 | 短時間の連続失敗が制限され、正常利用へ戻せる |
| セッション固定 | ログイン前後でSession IDが変わる |
| Cookie | Secure、HttpOnly、SameSite、Pathが意図どおりである |
| 有効期限 | 無操作期限と絶対期限の後にアクセスできない |
| ログアウト | 無効化したSession IDを再利用できない |
| CSRF | 状態変更時に不正なトークンを拒否する |
| 再設定 | 期限切れ、使用済み、改ざんされたトークンを拒否する |
| 認可 | 他人のIDを指定してもデータを閲覧・更新できない |
| ログ | パスワードやトークンが記録されない |
実装の順序
最初からすべてを作るのではなく、次の順序で小さく確認すると処理を追いやすくなります。
- ユーザーデータとパスワードハッシュ
- ログインと共通エラーメッセージ
- セッション発行とCookie属性
- 認証必須ページと認可チェック
- ログアウトとセッション失効
- ログイン試行の制限と監視
- パスワード再設定
- 必要に応じてメール確認、MFA、SSO
避けたい実装
- パスワードを平文または復号可能な形で保存する
- SHA-256などの高速な汎用ハッシュを単独でパスワード保存に使う
- ログイン前後で同じSession IDを使う
- Session IDや再設定トークンをログへ出す
- ログインをフロントエンドの表示制御だけで判定する
- ログイン試行を無制限に受け付ける
- アカウントの存在が分かるエラーを返す
- 再設定トークンに有効期限や一回限りの制約を設けない
- 要件を整理せず、認証だからという理由だけでJWTを使う
参考資料
- OWASP Authentication Cheat Sheet
- OWASP Session Management Cheat Sheet
- OWASP Password Storage Cheat Sheet
- OWASP Forgot Password Cheat Sheet
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet
- NIST SP 800-63B: Authentication and Authenticator Management
- MDN: Set-Cookie
更新履歴
- 初版作成