# 認証処理 コードリーディング解説
## 対象ファイル：auth.php / login.php / login_check.php

---

## 全体の流れ（3ファイルの関係）

```
【login.php】               【login_check.php】            【auth.php】
ログインフォーム表示    →   POST受信・認証処理          ←   認証関数を提供
  ・会社コード入力           ・入力チェック
  ・ユーザーID入力           ・DB照合
  ・パスワード入力            ・パスワード検証
  ・送信ボタン               ・テナントDB接続確認
       ↓                     ・セッション生成
  login_check.php へPOST     ・T_LOGIN_SESSION登録
                             ・menu.php へリダイレクト
                                      ↓
                         【各業務画面（tokui_list.php など）】
                             requireLogin() → ログイン済みか確認
                                              タイムアウト確認
                                              last_access更新
```

---

# ■ auth.php（認証関数群）

---

## ① startAppSession()：セッション開始

```php
function startAppSession(): void
{
    if (session_status() === PHP_SESSION_NONE) {
        session_name('RERENTWEBSESSID');
        session_start();
    }
}
```

| コード | 説明 | C#との対比 |
|--------|------|-----------|
| `session_status() === PHP_SESSION_NONE` | セッション未開始かを確認。二重呼び出しを防ぐ | `Session.IsNewSession` に近い |
| `session_name('RERENTWEBSESSID')` | クッキーに使うセッションID名を変更 | web.config の `sessionState cookieName` |
| `session_start()` | セッションを開始。これ以降 `$_SESSION` が使える | ASP.NET はページ読み込み時に自動開始 |

**なぜセッション名を変えるのか：**

```
デフォルト名 → PHPSESSID
           → 「PHPで作られている」とブラウザの開発者ツールから丸見え
           → 攻撃者にヒントを与える

カスタム名  → RERENTWEBSESSID
           → 使用技術を隠せる（セキュリティ上の小さな工夫）
```

---

## ② requireLogin()：ログイン確認（各業務画面の冒頭で呼ばれる）

```php
function requireLogin(): void
{
    startAppSession();
```

- 業務画面の先頭で必ず呼ぶ。セッション開始を内包しているので呼び出し側は意識不要。

```php
    if (empty($_SESSION['login']) || $_SESSION['login'] !== true) {
        redirect('/login.php');
    }
```

- `empty()` … 変数が未定義・null・false・空文字・0 のいずれかで `true` を返す。
- `$_SESSION['login']` が存在しない or `true` でないなら未ログインと判定。
- `||` … OR演算子（C# と同じ）。どちらか1つでも `true` なら未ログイン。
- 未ログインなら `redirect()` で強制的にログイン画面へ。
  - `redirect()` 内部の `exit` でここで処理終了（以降のコードは実行されない）。

```php
    if (!empty($_SESSION['last_access'])) {
        if (time() - $_SESSION['last_access'] > SESSION_TIMEOUT_SECONDS) {
            logoutSessionOnly();
            redirect('/login.php');
        }
    }
```

- `time()` … 現在のUnixタイムスタンプ（1970年1月1日からの秒数）。
  - C# の `DateTimeOffset.UtcNow.ToUnixTimeSeconds()` に相当。
- `time() - $_SESSION['last_access']` … 前回アクセスからの経過秒数。
- `SESSION_TIMEOUT_SECONDS` … `app.php` で定義した `28800`（8時間）。
- 経過秒数が8時間を超えていたら → セッション破棄 → ログイン画面へ。

```php
    $_SESSION['last_access'] = time();
```

- タイムアウトしていなければ最終アクセス日時を**今**に更新。
- 操作するたびに時計がリセットされる → 操作中はタイムアウトしない仕組み。

```php
    updateLoginSessionAccess();
}
```

- DB上のセッション管理テーブルにも最終アクセス日時を記録する（後述）。

---

## ③ updateLoginSessionAccess()：DBのセッション記録更新

```php
function updateLoginSessionAccess(): void
{
    if (empty($_SESSION['company_code']) || empty($_SESSION['user_id'])) {
        return;
    }

    try {
        $db = getCommonDb();

        $sql = "
            UPDATE T_LOGIN_SESSION
               SET LAST_ACCESS_DATE = sysdatetime()
             WHERE COMPANY_CODE = :COMPANY_CODE
               AND USER_ID = :USER_ID
               AND SESSION_ID = :SESSION_ID
        ";

        $stmt = $db->prepare($sql);
        $stmt->execute([
            ':COMPANY_CODE' => $_SESSION['company_code'],
            ':USER_ID'      => $_SESSION['user_id'],
            ':SESSION_ID'   => session_id(),
        ]);

    } catch (Throwable $e) {
        // ログイン中の画面表示を止めないため、ここでは握りつぶす
    }
}
```

- `session_id()` … 現在のセッションIDを取得。
  - クッキー `RERENTWEBSESSID` の値と同じ文字列。
  - C# の `Session.SessionID` に相当。
- 誰が・どのIPから・いつ最後にアクセスしたかを `T_LOGIN_SESSION` に記録。
  - 管理者が「誰がログイン中か」を確認できる。
- `catch` で握りつぶす理由：DB更新に失敗しても業務画面は表示し続けるべきだから。

---

## ④ logoutSessionOnly()：セッション完全破棄

```php
function logoutSessionOnly(): void
{
    $_SESSION = [];
```

- `$_SESSION = []` … セッション変数をすべて空にする（データの削除）。
- これだけではセッション自体はまだ残っている。

```php
    if (ini_get('session.use_cookies')) {
        $params = session_get_cookie_params();

        setcookie(
            session_name(),     // クッキー名: 'RERENTWEBSESSID'
            '',                 // 値を空にする
            time() - 42000,     // 有効期限を過去に設定 → ブラウザがクッキーを削除
            $params['path'],
            $params['domain'],
            $params['secure'],
            $params['httponly']
        );
    }
```

- `ini_get('session.use_cookies')` … PHPがセッションにクッキーを使う設定かを確認。
  - デフォルト `true`（ほぼ常にクッキーを使う）。
- `session_get_cookie_params()` … 現在のセッションクッキーの設定（パス・ドメイン等）を取得。
- `setcookie(name, '', 過去の時刻, ...)` … クッキーの有効期限を過去にすることで削除。
  - `time() - 42000` は「今から約11.7時間前」。とにかく過去ならOK。
  - C# の `Response.Cookies[name].Expires = DateTime.Now.AddDays(-1)` に相当。

```php
    session_destroy();
}
```

- サーバー側のセッションファイル（`sess_abc123.txt`）を削除。
- 3ステップで完全なログアウト：
  ```
  ① $_SESSION = []        → セッション変数を空にする
  ② setcookie(... 過去 )  → ブラウザのクッキーを削除
  ③ session_destroy()     → サーバーのセッションファイルを削除
  ```

---

# ■ login.php（ログインフォーム画面）

---

```php
startAppSession();

$error = $_SESSION['login_error'] ?? '';
unset($_SESSION['login_error']);
```

- **PRGパターンの応用**：`login_check.php` がエラー時にセッションにメッセージを入れて
  `login.php` にリダイレクトする。`login.php` はそれを読んで即削除する。
- `unset($_SESSION['login_error'])` … 読み取ったら即削除（再表示で消えるように）。
  - C# の `TempData["error"]` と全く同じ仕組み（1回だけ表示されて消えるデータ）。

```php
<form method="post" action="<?= h(BASE_PATH) ?>/login_check.php" autocomplete="off">
```

- `action="..."` … フォームの送信先を明示的に `login_check.php` に指定。
  - `action=""` （省略）なら自分自身に送信されるが、認証処理は別ファイルに分離している。
- `autocomplete="off"` … ブラウザによる入力補完を無効にする。
  - 共有PCでのパスワード保存を防ぐための配慮。

```php
<input type="password" name="password" required>
```

- `type="password"` … 入力内容をマスク表示（`●●●●`）にする。
- `required` … HTML5のバリデーション属性。空のまま送信できなくなる。
  - **ただしJSオフで無効化できる** → PHP側でも必ず検証する（多層防御）。

---

# ■ login_check.php（認証処理の本体）

---

## ① 入力値の受け取り・空チェック

```php
$companyCode = trim($_POST['company_code'] ?? '');
$userId      = trim($_POST['user_id']      ?? '');
$password    = $_POST['password']          ?? '';
```

- `$password` だけ `trim()` しない理由：パスワードに**意図的にスペースを含める**ユーザーもいるため。
  - 会社コードやIDはスペースが入ることはないが、パスワードは原文ままで扱う。

```php
if ($companyCode === '' || $userId === '' || $password === '') {
    writeLoginErrorLog('入力値不足', [...]);
    $_SESSION['login_error'] = 'ログイン情報が正しくありません。';
    redirect('/login.php');
}
```

- エラーメッセージを `'会社コードを入力してください'` にしない理由：
  ```
  「会社コードが違います」→ 攻撃者に「ユーザーIDは合っている」と教えてしまう
  「ログイン情報が正しくありません」→ どの項目が違うか教えない（情報漏洩防止）
  ```
  これを**セキュリティ上の意図的なあいまい化**と呼ぶ。

---

## ② ユーザー情報の取得（JOINクエリ）

```php
$sql = "
    SELECT
        U.COMPANY_CODE, U.USER_ID, U.USER_NAME,
        U.PASSWORD_HASH, U.ROLE_KBN,
        C.COMPANY_NAME, C.DBSERVER, C.DBNAME, C.DBID, C.DBPWD
    FROM M_WEB_USER U
    INNER JOIN M_COMPANY C
        ON U.COMPANY_CODE = C.COMPANY_CODE
    WHERE U.COMPANY_CODE = :COMPANY_CODE
      AND U.USER_ID      = :USER_ID
      AND ISNULL(U.ACTIVE_FLG, 1) = 1
      AND ISNULL(C.ACTIVE_FLG, 1) = 1
";
```

- `M_WEB_USER` と `M_COMPANY` を1回のクエリで JOIN している。
  - ユーザー情報とテナントDB接続情報を同時に取得する効率的な設計。
- `PASSWORD_HASH` … DBにはパスワードの**ハッシュ値**が保存されている（生パスワードは保存しない）。
- **WHERE句にパスワードを含めていない** → 正しい設計。
  - パスワード照合はPHP側で `password_verify()` を使って行う（後述）。

---

## ③ password_verify()：パスワード照合

```php
if (!password_verify($password, $user['PASSWORD_HASH'])) {
    ...
    redirect('/login.php');
}
```

- `password_verify(入力パスワード, ハッシュ値)` … 入力パスワードをハッシュして比較。
- PHPの組み込み関数。アルゴリズムは **bcrypt**（デフォルト）。
- C# の `BCrypt.Net.BCrypt.Verify(password, hash)` に相当。

**なぜハッシュが必要か：**

```
【DBに生パスワードを保存するとき（NG）】
DB漏洩 → パスワードが丸見え → 全ユーザーのパスワードが即座に流出

【DBにハッシュを保存するとき（OK）】
DB漏洩 → ハッシュ値しか見えない → 元のパスワードは復元不可能
         ↑ bcryptは意図的に計算が遅い → 総当たり攻撃に時間がかかる
```

**SQLにWHEREでパスワードを入れない理由（重要）：**

```sql
-- ❌ SQLでパスワードを比較する方法（古い・危険）
WHERE PASSWORD = :PASSWORD
-- → タイミング攻撃の余地がある
-- → SQLに生パスワードが乗る（ログに残る可能性）

-- ✅ PHPで password_verify() を使う方法（現在の標準）
-- → PHPが定数時間で比較（タイミング攻撃対策済み）
-- → SQLにパスワードが乗らない
```

---

## ④ テナントDB接続確認

```php
try {
    $tenantDb = new PDO($tenantDsn, $user['DBID'], $user['DBPWD'], [...]);
    $tenantDb->query('SELECT 1');

} catch (Throwable $e) {
    writeLoginErrorLog('業務DB接続失敗', [...]);
    $_SESSION['login_error'] = 'ログイン情報が正しくありません。';
    redirect('/login.php');
}
```

- `$tenantDb->query('SELECT 1')` … DB接続の疎通確認。
  - `SELECT 1` は何もしない最小のSQL（接続できれば1が返るだけ）。
  - C# でいう `connection.Open()` と同じ目的。
- ログイン時に接続確認することで、`M_COMPANY` の接続情報ミスを**早期発見**できる。
  - 業務画面でいきなりエラーになる前にここで検出できる。

---

## ⑤ session_regenerate_id()：セッション固定化攻撃対策

```php
session_regenerate_id(true);
```

- **セッション固定化攻撃とは：**

```
① 攻撃者がセッションID（例：abc123）を事前に取得
② 被害者をそのセッションIDでログインさせる
③ 攻撃者が同じセッションIDで被害者になりすます

対策：ログイン成功後にセッションIDを新しくすれば
      事前に取得したIDは無効になる
```

- `true` → 古いセッションファイルを**サーバーから削除**してから新IDを発行。
  - `false` → 古いファイルを残す（二重アクセスの可能性が残るため非推奨）。
- コメントにも「セッション固定攻撃対策」と明記されており、意図が明確な良い実装。

---

## ⑥ セッション変数の設定

```php
$_SESSION['login']        = true;
$_SESSION['company_code'] = $user['COMPANY_CODE'];
$_SESSION['company_name'] = $user['COMPANY_NAME'];
$_SESSION['user_id']      = $user['USER_ID'];
$_SESSION['user_name']    = $user['USER_NAME'];
$_SESSION['role_kbn']     = $user['ROLE_KBN'];
$_SESSION['last_access']  = time();
```

- ログイン成功後にセッションに必要な情報を格納する。
- これ以降のすべての画面で `$_SESSION['user_name']` などが使えるようになる。
- `role_kbn` … ロール（権限）区分。画面ごとのアクセス制御に使える。

---

## ⑦ T_LOGIN_SESSION への登録（トランザクション）

```php
$commonDb->beginTransaction();

// SELECT で存在確認 → UPDATE or INSERT
$exists = (int)$stmtCheck->fetchColumn();

if ($exists > 0) {
    // UPDATE（既存レコードの更新）
} else {
    // INSERT（初回ログイン）
}

$commonDb->commit();
```

- `beginTransaction()` → `commit()` / `rollBack()` の3点セット。
  - C# の `SqlTransaction` または `TransactionScope` に相当。
- **MERGE を使わず SELECT → UPDATE/INSERT に分けている理由**（コメントに明記）：
  - SQL Server の MERGE はデッドロックが発生しやすい既知の問題がある。
  - 明示的に分けることで安全性と可読性を優先している。

```php
} catch (Throwable $e) {
    if ($commonDb->inTransaction()) {
        $commonDb->rollBack();
    }
    ...
    redirect('/login.php');
}
```

- `inTransaction()` … トランザクション中かどうか確認してから `rollBack()`。
  - トランザクション外で `rollBack()` するとエラーになるため安全装置として必要。
  - C# の `transaction != null` で null チェックするのと同じ発想。
- `$e->getMessage()` → C# の `ex.Message`
- `$e->getFile()` → エラーが発生したファイル名
- `$e->getLine()` → エラーが発生した行番号
- `$e->getTraceAsString()` → C# の `ex.StackTrace`

---

# ■ まとめ：ログイン処理の全体シーケンス

```
login.php（GET）
  └─ フォームを表示
  └─ セッションのエラーメッセージを表示して削除
         ↓ submitボタン押下
login_check.php（POST）
  ├─ ① 入力チェック（空チェック）
  │       NG → セッションにエラー → login.php へリダイレクト
  ├─ ② 共通DBからユーザー・会社情報を取得（JOIN）
  │       NG → セッションにエラー → login.php へリダイレクト
  ├─ ③ password_verify() でパスワード照合
  │       NG → セッションにエラー → login.php へリダイレクト
  ├─ ④ テナントDB接続確認（SELECT 1）
  │       NG → セッションにエラー → login.php へリダイレクト
  ├─ ⑤ session_regenerate_id(true)（セッション固定化攻撃対策）
  ├─ ⑥ $_SESSION に必要情報を格納
  ├─ ⑦ T_LOGIN_SESSION を UPDATE or INSERT（トランザクション）
  └─ ⑧ menu.php へリダイレクト

menu.php / tokui_list.php 等（GET）
  └─ requireLogin()
      ├─ ログイン確認（$_SESSION['login'] === true）
      ├─ タイムアウト確認（last_access から SESSION_TIMEOUT_SECONDS 以内か）
      └─ last_access を現在時刻に更新 → 画面を表示
```

---

# ■ セキュリティ対策 総まとめ

このコードで実装されているセキュリティ対策の一覧：

| 対策 | 実装箇所 | 目的 |
|------|---------|------|
| パスワードハッシュ（bcrypt） | `password_verify()` | DB漏洩時のパスワード保護 |
| セッション固定化攻撃対策 | `session_regenerate_id(true)` | なりすましログイン防止 |
| セッション名カスタマイズ | `session_name('RERENTWEBSESSID')` | 使用技術の隠蔽 |
| セッションタイムアウト | `requireLogin()` の時間チェック | 放置セッションの自動無効化 |
| エラーメッセージのあいまい化 | 全エラーを同一メッセージに統一 | どの項目が違うか教えない |
| ログイン試行のログ記録 | `writeLoginErrorLog()` | 不正アクセス調査の証跡 |
| クッキー完全削除 | `logoutSessionOnly()` の3ステップ | セッション残留防止 |
| テナントDB接続確認 | ログイン時の `SELECT 1` | 設定ミスの早期検出 |
| SQLパラメータ化 | 全SQL で `bindValue()` / `execute([...])` | SQLインジェクション防止 |
| HTMLエスケープ | 全出力で `h()` | XSS防止 |
