以前、LaravelからDBに格納するデータを暗号化・復号する方法とメールアドレスのuniqueバリデーションの問題というのを書きました。
これで大丈夫かと思ったのですが、メールアドレスを暗号化したために、別のところで影響がありました。
パスワードのリセットです。
メールアドレスを入力して「パスワード再設定用リンクを送信する」ボタンを押すと、「メールアドレスに対応するユーザーが見つかりません。」とエラーになってしまいます。
メールアドレスを暗号化して保存しているために、標準の機能が動かなくなっていました。
resources/views/auth/passwords/email.blade.php を見ると、「パスワード再設定用リンクを送信する」を押すと、password/email にPOSTしてます。
ルーティングを確認します。
$ php artisan route:list | grep password/email | | POST | password/email | password.email | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail | web |
ForgotPasswordControllerを見てみます。
<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\SendsPasswordResetEmails; class ForgotPasswordController extends Controller { /* |-------------------------------------------------------------------------- | Password Reset Controller |-------------------------------------------------------------------------- | | This controller is responsible for handling password reset emails and | includes a trait which assists in sending these notifications from | your application to your users. Feel free to explore this trait. | */ use SendsPasswordResetEmails; }
本体は vendor/laravel/ui/auth-backend/SendPasswordResetEmails でした。
メールアドレスが見つからないために、$this->sendResetLinkFailedResponse() を return しているようです。
Password::broker() に対して問い合わせをして、失敗しています。
/** * Send a reset link to the given user. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse */ public function sendResetLinkEmail(Request $request) { $this->validateEmail($request); // We will send the password reset link to this user. Once we have attempted // to send the link, we will examine the response then see the message we // need to show to the user. Finally, we'll send out a proper response. $response = $this->broker()->sendResetLink( $this->credentials($request) ); return $response == Password::RESET_LINK_SENT ? $this->sendResetLinkResponse($request, $response) : $this->sendResetLinkFailedResponse($request, $response); } (略) /** * Get the broker to be used during password reset. * * @return \Illuminate\Contracts\Auth\PasswordBroker */ public function broker() { return Password::broker(); }
vendor/laravel/framework/src/Illuminate/Support/Facades/Password.php を見てみます。
これもたいしたことはしてなくて、本体は vendor/laravel/framework/src/Illuminate/Auth/Passwords/PasswordBroker にあります。
/** * Send a password reset link to a user. * * @param array $credentials * @param \Closure|null $callback * @return string */ public function sendResetLink(array $credentials, Closure $callback = null) { // First we will check to see if we found a user at the given credentials and // if we did not we will redirect back to this current URI with a piece of // "flash" data in the session to indicate to the developers the errors. $user = $this->getUser($credentials); if (is_null($user)) { return static::INVALID_USER; } if ($this->tokens->recentlyCreatedToken($user)) { return static::RESET_THROTTLED; } $token = $this->tokens->create($user); if ($callback) { $callback($user, $token); } else { // Once we have the reset token, we are ready to send the message out to this // user with a link to reset their password. We will then redirect back to // the current URI having nothing set in the session to indicate errors. $user->sendPasswordResetNotification($token); } return static::RESET_LINK_SENT; } (略) /** * Get the user for the given credentials. * * @param array $credentials * @return \Illuminate\Contracts\Auth\CanResetPassword|null * * @throws \UnexpectedValueException */ public function getUser(array $credentials) { $credentials = Arr::except($credentials, ['token']); $user = $this->users->retrieveByCredentials($credentials); if ($user && ! $user instanceof CanResetPasswordContract) { throw new UnexpectedValueException('User must implement CanResetPassword interface.'); } return $user; }
$credentials を出力してみると、リクエストパラメータが渡されていることがわかりました。
array ( 'email' => 'hoge@example.com', )
$this->users を調べてみたら、vendor/laravel/framework/src/Illuminate/Auth/EloquentUserProvider であることがわかりました。
メールアドレスを元にクエリをしているところは、この部分です。
/** * Retrieve a user by the given credentials. * * @param array $credentials * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveByCredentials(array $credentials) { if (empty($credentials) || (count($credentials) === 1 && Str::contains($this->firstCredentialKey($credentials), 'password'))) { return; } // First we will add each credential element to the query as a where clause. // Then we can execute the query and, if we found a user, return it in a // Eloquent User "model" that will be utilized by the Guard instances. $query = $this->newModelQuery(); foreach ($credentials as $key => $value) { if (Str::contains($key, 'password')) { continue; } if (is_array($value) || $value instanceof Arrayable) { $query->whereIn($key, $value); } elseif ($value instanceof Closure) { $value($query); } else { $query->where($key, $value); } } return $query->first(); }
要するに、この部分をオーバーライドしてやればよさそうです。
Laravelでの vendor 以下に存在するクラスのオーバーライドの方法を調べてみました。
まず、app/Providers に、オーバーライドの実装をするクラスを作成します。
メールアドレスを暗号化しましたが、md5値を格納するカラムを作ってますので、これをキーにしてクエリしてみます。
app/Providers/Md5UserProvider を作成します。
email でのクエリの部分を md5 に置き換えてやります。
同一の md5 の値に対して複数がヒットすることはあり得ないとは思いますが、クエリの結果全てに対して email が一致するかどうかで調べます。
email 以外のクエリに対しては、元のコードが動くようにしておきます。
<?php namespace App\Providers; use Illuminate\Auth\EloquentUserProvider; use Illuminate\Support\Str; class Md5UserProvider extends EloquentUserProvider { /** * Retrieve a user by the given credentials. * * @param array $credentials * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveByCredentials(array $credentials) { if (empty($credentials) || (count($credentials) === 1 && Str::contains($this->firstCredentialKey($credentials), 'password'))) { return; } // First we will add each credential element to the query as a where clause. // Then we can execute the query and, if we found a user, return it in a // Eloquent User "model" that will be utilized by the Guard instances. $query = $this->newModelQuery(); $email = null; foreach ($credentials as $key => $value) { if (Str::contains($key, 'password')) { continue; } if (is_array($value) || $value instanceof Arrayable) { $query->whereIn($key, $value); } elseif ($value instanceof Closure) { $value($query); } else { if ($key == 'email') { $email = $value; $key = 'md5'; $value = md5($value); } $query->where($key, $value); } } if (!empty($email)) { $results = $query->get(); foreach ($results as $u) { if ($u->email == $email) return $u; } return; } return $query->first(); } }
では、従来は EloquentUserProvider が呼び出されていたところを、Md5UserProvier が呼び出されるように変更する必要があります。
公式ドキュメントでは、ここらへんに書かれてます。
app/Providers/AuthServiceProvider を修正します。
<?php namespace App\Providers; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Auth; use App\Providers\Md5UserProvider; class AuthServiceProvider extends ServiceProvider { /** * The policy mappings for the application. * * @var array */ protected $policies = [ // 'App\Model' => 'App\Policies\ModelPolicy', ]; /** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); Auth::provider('md5', function($app, array $config) { return new Md5UserProvider($app['hash'], $config['model']); }); } }
あとは、config/auth.php を修正すれば終わりです。
標準の driver の eloquent を、上で作成した md5 に変更するだけです。
/* |-------------------------------------------------------------------------- | User Providers |-------------------------------------------------------------------------- | | All authentication drivers have a user provider. This defines how the | users are actually retrieved out of your database or other storage | mechanisms used by this application to persist your user's data. | | If you have multiple user tables or models you may configure multiple | sources which represent each model / table. These sources may then | be assigned to any extra authentication guards you have defined. | | Supported: "database", "eloquent" | */ 'providers' => [ 'users' => [ //'driver' => 'eloquent', 'driver' => 'md5', 'model' => App\User::class, ], // 'users' => [ // 'driver' => 'database', // 'table' => 'users', // ], ],
これで、メールアドレスを暗号化していても、パスワード再設定用リンクの送信ができるようになりました。