Laravel 5.8において、ユーザー認証のパスワードの長さの制限がそれまでの最低の6文字長から8文字長に変わりました。セキュリティ強化の目的です。しかし、この制限を変えたい、つまり昔の6文字長をキープしたい、あるいは8文字長でなくより強化して10文字長としたいなら、どうするのでしょう?
5.8のコードを見ると、会員登録とパスワードを忘れた時のパスワードリセットにおいてのみ、min:8
のバリデーションが使われています(不思議とログインではパスワードの長さの制限なし)。しかもトレイトで定義されているバリデーションの関数rules()
を上書きすればよいだけ、と思いきや、ハードコードされていて効かない箇所がありました。
ハードコードの箇所は、パスワードリセットの機能です。そこのコントローラーから、コードを追跡してみましょう。
namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\ResetsPasswords; class ResetPasswordController extends Controller { use ResetsPasswords; /** * Where to redirect users after resetting their password. * * @var string */ protected $redirectTo = '/home'; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest'); } }
そこで使用されている、ResetPasswordsのトレイトの定義は、
namespace Illuminate\Foundation\Auth; use Illuminate\Support\Str; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Password; use Illuminate\Auth\Events\PasswordReset; trait ResetsPasswords { ... /** * Reset the given user's password. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse */ public function reset(Request $request) { $request->validate($this->rules(), $this->validationErrorMessages()); // Here we will attempt to reset the user's password. If it is successful we // will update the password on an actual user model and persist it to the // database. Otherwise we will parse the error and return the response. $response = $this->broker()->reset( $this->credentials($request), function ($user, $password) { $this->resetPassword($user, $password); } ); // If the password was successfully reset, we will redirect the user back to // the application's home authenticated view. If there is an error we can // redirect them back to where they came from with their error message. return $response == Password::PASSWORD_RESET ? $this->sendResetResponse($request, $response) : $this->sendResetFailedResponse($request, $response); } /** * Get the password reset validation rules. * * @return array */ protected function rules() { return [ 'token' => 'required', 'email' => 'required|email', 'password' => 'required|confirmed|min:8', ]; } ...
rules()
では、min:8
とあるから、先ほどのResetPasswordControllerで、rules()
を再定義して、min:6
とすれば良いということです。
しかし、そのルールを使用する21行目のvalidate()
だけでなく、さらに、26行目のbrokerのreset()
でもチェックが入っていたのです。その、brokerのコードを見てみましょう。
namespace Illuminate\Auth\Passwords; use Closure; use Illuminate\Support\Arr; use UnexpectedValueException; use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Auth\PasswordBroker as PasswordBrokerContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; class PasswordBroker implements PasswordBrokerContract { ... /** * Reset the password for the given token. * * @param array $credentials * @param \Closure $callback * @return mixed */ public function reset(array $credentials, Closure $callback) { // If the responses from the validate method is not a user instance, we will // assume that it is a redirect and simply return it from this method and // the user is properly redirected having an error message on the post. $user = $this->validateReset($credentials); if (! $user instanceof CanResetPasswordContract) { return $user; } $password = $credentials['password']; // Once the reset has been validated, we'll call the given callback with the // new password. This gives the user an opportunity to store the password // in their persistent storage. Then we'll delete the token and return. $callback($user, $password); $this->tokens->delete($user); return static::PASSWORD_RESET; } /** * Validate a password reset for the given credentials. * * @param array $credentials * @return \Illuminate\Contracts\Auth\CanResetPassword|string */ protected function validateReset(array $credentials) { if (is_null($user = $this->getUser($credentials))) { return static::INVALID_USER; } if (! $this->validateNewPassword($credentials)) { return static::INVALID_PASSWORD; } if (! $this->tokens->exists($user, $credentials['token'])) { return static::INVALID_TOKEN; } return $user; } /** * Set a custom password validator. * * @param \Closure $callback * @return void */ public function validator(Closure $callback) { $this->passwordValidator = $callback; } /** * Determine if the passwords match for the request. * * @param array $credentials * @return bool */ public function validateNewPassword(array $credentials) { if (isset($this->passwordValidator)) { [$password, $confirm] = [ $credentials['password'], $credentials['password_confirmation'], ]; return call_user_func( $this->passwordValidator, $credentials ) && $password === $confirm; } return $this->validatePasswordWithDefaults($credentials); } /** * Determine if the passwords are valid for the request. * * @param array $credentials * @return bool */ protected function validatePasswordWithDefaults(array $credentials) { [$password, $confirm] = [ $credentials['password'], $credentials['password_confirmation'], ]; return $password === $confirm && mb_strlen($password) >= 8; } ...
上のコードで、定義されている関数をトレースすると実行順番は、
reset()
validateReset()
validateNewPassword()
validatePasswordWithDefaults()
最後の関数で8文字長の制限がありましたね(118行目)。しっかり、ハードコードされています。
5.8のドキュメントでは、
https://laravel.com/docs/5.8/upgrade#new-default-password-length
Illuminate\Auth\Passwords\PasswordBrokeを継承して、validatePasswordWithDefaults()
を上書きすればよいとありますが、ディープ過ぎて面倒です。
以下で他の人も不平述べています。
https://github.com/laravel/framework/issues/28274
より簡単な解決策はないかと、調査したら、
ベストの解決策は、ResetPasswordsのトレイトのreset()
をResetPasswordControllerで上書きしてbrokerのvalidator()を無能にすることです。つまり、
namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Http\Request; // この追加必要! use Illuminate\Support\Facades\Password; // この追加必要! class ResetPasswordController extends Controller { ... /** * Reset the given user's password. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse */ public function reset(Request $request) { $request->validate($this->rules(), $this->validationErrorMessages()); // Here we will attempt to reset the user's password. If it is successful we // will update the password on an actual user model and persist it to the // database. Otherwise we will parse the error and return the response. // PasswordBrokerのvalidatorをここで定義 $this->broker()->validator(function (array $credentials) { return true; // rules()ですでにバリデーションを実行しているので、さらにここでバリデーションの必要はなし! }); $response = $this->broker()->reset( $this->credentials($request), function ($user, $password) { $this->resetPassword($user, $password); } ); // If the password was successfully reset, we will redirect the user back to // the application's home authenticated view. If there is an error we can // redirect them back to where they came from with their error message. return $response == Password::PASSWORD_RESET ? $this->sendResetResponse($request, $response) : $this->sendResetFailedResponse($request, $response); } /** * Get the password reset validation rules. * * @return array */ protected function rules() { return [ 'token' => 'required', 'email' => 'required|email', 'password' => 'required|confirmed|min:6', // min:8でなくmin:6に変える ]; } ...
rules()
も再定義して6文字長としました。
さて、この問題6.xのバージョンではどうなんでしょう? PasswordBroker.phpのコードを見ると、
namespace Illuminate\Auth\Passwords; use Closure; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Illuminate\Contracts\Auth\PasswordBroker as PasswordBrokerContract; use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Support\Arr; use UnexpectedValueException; class PasswordBroker implements PasswordBrokerContract { ... /** * Reset the password for the given token. * * @param array $credentials * @param \Closure $callback * @return mixed */ public function reset(array $credentials, Closure $callback) { $user = $this->validateReset($credentials); // If the responses from the validate method is not a user instance, we will // assume that it is a redirect and simply return it from this method and // the user is properly redirected having an error message on the post. if (! $user instanceof CanResetPasswordContract) { return $user; } $password = $credentials['password']; // Once the reset has been validated, we'll call the given callback with the // new password. This gives the user an opportunity to store the password // in their persistent storage. Then we'll delete the token and return. $callback($user, $password); $this->tokens->delete($user); return static::PASSWORD_RESET; } /** * Validate a password reset for the given credentials. * * @param array $credentials * @return \Illuminate\Contracts\Auth\CanResetPassword|string */ protected function validateReset(array $credentials) { if (is_null($user = $this->getUser($credentials))) { return static::INVALID_USER; } if (! $this->tokens->exists($user, $credentials['token'])) { return static::INVALID_TOKEN; } return $user; } ...
validateReset()から、validateNewPassword()は削除されていますね!
これに関しては、以下で説明されています。
https://laravel.com/docs/6.x/upgrade#belongs-to-update (Password Resetの部分)
さらに、PRとして、
https://github.com/laravel/framework/pull/29480
もう、この部分で悩むことはなさそうです。
メルマガ購読の申し込みはこちらから。