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
もう、この部分で悩むことはなさそうです。
メルマガ購読の申し込みはこちらから。