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に以下のような関数を追加すればメールアドレス以外の項目でログインするようになります。
/**
* 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">
/**
* 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
メソッドを追加する必要があります。
/**
* 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にログインに必要なフォーム入力値が渡される。
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
こちらの記事が参考になります。