Laravel ログイン認証に使用する項目を変更する

Laravelを使ってログイン認証を作成した場合、初期状態ではemailとpassowordのセットをPOST送信することでログインできる形にLoginControllerが作成されますが、別の項目を使ってログインするようにしたい場合、ちょっとした工夫が必要となります。

## 経緯
メールによる認証トークンを使った二段階認証の実装時、最終的なログインの値をemail・passwordではなく二段階認証トークンにする必要がありました。
ログインに使用するパスワード項目を変更するのにEloquentUserProviderをオーバーライドする方法しか見当たらなかったため、備忘録として記載しておきます。

二段階認証の具体的な実装方法については省きますが、以下のようなフローに組み替えることで最小限のオーバーライドで対応することができました。
- 1ページ目でID・パスワードを送信して`Hash::check()`によるパスワードの整合性チェック (ここは自前で実装)
- 2ページ目で2段階認証トークンを入力させてで本来のログイン機構(attemptLogin)を使用して権限を付与する

## `email`以外の値を認証に使用する

nameやlogin_idなど、メールアドレス以外の項目でユーザーを特定する場合です。
この項目でアカウントを特定できる必要があるため、テーブルの中でユニークな項目である必要があります。
LaravelUIのログイン画面は`Illuminate\Foundation\Auth\AuthenticatesUsers`トレイトでデフォルトのログイン処理が構成されていて、細かい挙動はこのトレイトで付加された関数をオーバーライドする形調整することができます。
モデル・データベース側にも追加したい項目を用意した上でLoginControllerに以下のような関数を追加すればメールアドレス以外の項目でログインするようになります。
```php:LoginController.php
/**
 * Get the login username to be used by the controller.
 *
 * @return string
 */
public function username()
{
    // 認証に利用したいフィールド名に変更
    return 'name';
}
```

## `password`以外の値を認証に使用する
password 側を変更するのはすこし難儀します。
なぜなら入力値はSessionGuardを通して、EloquentUserProviderで判定されるのですが、認証に仕様する値のキーがpasswordで固定されているためです。
https://github.com/laravel/framework/blob/10.x/src/Illuminate/Auth/EloquentUserProvider.php#L151
(config/auth.phpで設定が変更されている場合は別のクラスとなります、念のため)

そのため、EloquentUserProviderを自前でオーバーライドしたくない場合は、SessionGuard::attemptに渡す暗号化された値のキーは必ず`password`である必要があります。
ということで、シンプルにやるならフォームからトークンを送信する際に`password`として送ってあげればOKです。
`<input type="text" name="password">`

今回は二段階認証で別にパスワードを持っているため、混同しないようコントローラ側で対応しました。

`<input type="text" name="tfa_token">`
```php:LoginController.php
/**
 * Attempt to log the user into the application.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return bool
 */
protected function credentials(Request $request)
{
    return $request->only('id') + ['password' => $request->tfa_token];
}
```

そのままではモデル側の($user->password)と比較されてしまうので、Authenticatableを継承したモデル側にも`getAuthPassword`メソッドを追加する必要があります。
```php:User.php
/**
 * Get the password for the user.
 *
 * @return string
 */
public function getAuthPassword()
{
    return $this->tfa_token;
}
```

注意点として、この値はハッシュとして対応をチェックされるためレコードの登録時にpasswordと同様暗号化されている必要があります。setterメソッドでbcryptメソッドを通すなどして、データベースに保存する前に暗号化するようにしておきましょう。
これで`$user->tfa_token`とフォームから送信した$request->tfa_tokenの値がハッシュとして対応した値となります。

ここまでやればAuthenticatableトレイトがうまくやってくれるはずです。

## 内訳の解説

ログインの判定ですが大まかに以下のようなかんじです。(今回直接関係しない部分は省いています)

- `Illuminate\Foundation\Auth\AuthenticatesUsers`が組み込まれたLoginControllerには`login`メソッドが追加されるので、ルーティングから`login`メソッドが呼ばれる。
- loginメソッドから同じトレイトのattemptLoginが呼ばれてSessionGuard::attemptにログインに必要なフォーム入力値が渡される。
  ```php:AuthenticatesUsers.php
  protected function attemptLogin(Request $request)
  {
      return $this->guard()->attempt(
          $this->credentials($request), $request->filled('remember')
      );
  }
  ```
- EloquentUserProvider::retrieveByCredentials `password`以外の項目を材料に該当するユーザーのレコードを検索。
  ここでレコードが見つからなければ失敗。
- EloquentUserProvider::validateCredentialsで`password`項目とモデルの`getAuthPassword()`がハッシュとして対応していればログイン成功。
  (ハッシュとして対応をチェックするので、モデル側の値はハッシュ化されている必要あり)
  LoginController::attemptLoginで`true`が返る。
- LoginController::sendLoginResponseでログインセッションの書き込みとリダイレクトが実行される。

認証に使用するモデルは`Illuminate\Foundation\Auth\User`(`Authenticatable`)を継承しているので、その中で継承しているgetAuthPasswordをオーバーライド
https://laravel.com/api/10.x/Illuminate/Auth/Authenticatable.html#method_getAuthPassword

また、EloquentUserProviderをオーバーライドでも対応可能です。
暗号化された値を用いない場合などはそのほうがよさそうですね。
https://qiita.com/hkusaba/items/c5a7f43a6312a259f200
こちらの記事が参考になります。