LaravelのPasswordBrokerについて調べる

9 min読了の目安(約5800字TECH技術記事

こんにちは。ファガイです。
今回はLaravelの5.3から仕組みとしてはちょっと入っていたPasswordBrokerについてソースレベルで調べてみます。

チェックしたバージョン

確認したのはLaravel 8のPasswordBrokerの実装です。
まあ実際の実装は5系の頃とそこまで変わってません。

そもそもPasswordBrokerって何

毎回のようにLaravelは名前が日本人には理解しにくいです。。。
Brokerは仲介人みたいな意味があるようです。実際のコードを読んでいくとその理解が付きやすいですが、パスワードのリセット処理とかの流れをこのPasswordBrokerを通してやりますよっていう感じです。
PasswordBrokerのやることは以下です。

  • パスワードリセットリンク通知の送信の仲介
  • パスワードリセット
  • パスワードリセットトークンに関してのバリデーション、スロットリング
  • トークンの作成の仲介

といったところになっています。

PasswordBrokerの作られ方

PasswordBrokerPasswordResetServiceProviderPasswordManagerPasswordBrokerといった感じで3つのクラスを通して作られます。

PasswordBrokerはコンストラクタとして2つのパラメータを持ってます。

/**
 * Create a new password broker instance.
 *
 * @param  \Illuminate\Auth\Passwords\TokenRepositoryInterface  $tokens
 * @param  \Illuminate\Contracts\Auth\UserProvider  $users
 * @return void
 */
public function __construct(TokenRepositoryInterface $tokens,
                         UserProvider $users)
{
    $this->users = $users;
    $this->tokens = $tokens;
}

UserProviderはAuthでもよく使われるやつですね。TokenRepositoryInterfaceはまあ名前の通りパスワードtokenのリポジトリでしょう。
今のところはDatabaseTokenRepositoryTokenRepositoryInterfaceの実装となります。(このTokenRepositoryInterfaceはよくあるリポジトリパターンの実装としては結構参考になると思います)

PasswordBrokerManagerの役割

PasswordBrokerManagerの役割は名前の通り管理です。
brokerを複数管理できるようにし、呼ばれた際に生成されてなかったらconfigを元にインスタンスを生成して返します。
まあよくある実装ですね。

public function broker($name = null)
{
    $name = $name ?: $this->getDefaultDriver();

    return $this->brokers[$name] ?? ($this->brokers[$name] = $this->resolve($name));
}

Tokenの作成

まずトークンのベースとなる作成を見ます。
以下はPasswordBrokerのメソッドです。

/**
 * Create a new password reset token for the given user.
 *
 * @param  \Illuminate\Contracts\Auth\CanResetPassword  $user
 * @return string
 */
public function createToken(CanResetPasswordContract $user)
{
    return $this->tokens->create($user);
}

TokenRepositoryInterfaceのcreateを呼んでます。
実際のDatabaseTokenRepositoryはこの様になっています。

/**
 * Create a new token record.
 *
 * @param  \Illuminate\Contracts\Auth\CanResetPassword  $user
 * @return string
 */
public function create(CanResetPasswordContract $user)
{
    // パスワードリセットに利用するメールアドレスを取得
    $email = $user->getEmailForPasswordReset();

    // tokenが入ってるテーブルに対象ユーザ(email)のデータがあったら消す
    $this->deleteExisting($user);

    // ランダムなトークンを生成します。
    $token = $this->createNewToken();

    $this->getTable()->insert($this->getPayload($email, $token));

    return $token;
}

さて、インサート時のgetPayloadメソッドを見てみましょう。

/**
 * Build the record payload for the table.
 *
 * @param  string  $email
 * @param  string  $token
 * @return array
 */
protected function getPayload($email, $token)
{
    return ['email' => $email, 'token' => $this->hasher->make($token), 'created_at' => new Carbon];
}

tokenをハッシュ化して入れています。セキュリティを意識した実装です。
実際のDBを見れる人間でも悪用が出来ないようにハッシュ化をしてデータを入れています。

パスワードリセット時には元のトークンをリンクに埋め込んで、それをハッシュ化して比較チェックします。

sendResetLinkの処理

おそらく一番使うであろうsendResetLinkの処理を見ていきましょう。

/**
 * 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)
{
    // ユーザを取得
    $user = $this->getUser($credentials);

    if (is_null($user)) {
        return static::INVALID_USER;
    }

    // 最近作られたトークンがあるか(Throttleに引っかかるか)
    if ($this->tokens->recentlyCreatedToken($user)) {
        return static::RESET_THROTTLED;
    }

    $token = $this->tokens->create($user);

    if ($callback) {
        $callback($user, $token);
    } else {
        // 通知を発火する
        $user->sendPasswordResetNotification($token);
    }

    return static::RESET_LINK_SENT;
}

getUserの処理

重要なのはこの2行です。

// 受け取ったcredentialsからtokenを除く(理由はfindに引っかからない可能性があるため)
$credentials = Arr::except($credentials, ['token']);

// 主にemail情報を元にUser情報を取得する
$user = $this->users->retrieveByCredentials($credentials);

// あとは存在しなかった場合のエラー処理、返り値はuser。

$this->tokens->recentlyCreatedTokenの処理

この処理は最初よくわかりませんでした。
簡単に説明するとthrottlingが設定されている場合(デフォルト60秒)、以前作成したレコードの追加日時+throttling秒現在日時と比較します。

public function recentlyCreatedToken(CanResetPasswordContract $user)
{
    $record = (array) $this->getTable()->where(
        'email', $user->getEmailForPasswordReset()
    )->first();

    return $record && $this->tokenRecentlyCreated($record['created_at']);
}
protected function tokenRecentlyCreated($createdAt)
{
    if ($this->throttle <= 0) {
        return false;
    }

    return Carbon::parse($createdAt)->addSeconds(
        $this->throttle
    )->isFuture(); // isFutureは未来かどうか
}

つまりこの処理は設定されているthrottleの秒数以内に呼び出されているかどうかを判断しています。
結果、以下の処理で弾いているわけです。

if ($this->tokens->recentlyCreatedToken($user)) {
    return static::RESET_THROTTLED;
}

sendPasswordResetNotificationの処理

sendPasswordResetNotificationは最終的にはメール送信へ渡しているだけです。

/**
 * Send the password reset notification.
 *
 * @param  string  $token
 * @return void
 */
public function sendPasswordResetNotification($token)
{
    $this->notify(new ResetPasswordNotification($token));
}

このメソッドはtraitであるため、notifyメソッドは何なのかわかっていません。
例えばUserモデルの場合だとRoutesNotificationsトレイトのメソッドになります。

public function notify($instance)
{
    app(Dispatcher::class)->send($this, $instance);
}

ここからは普通のNotificationと一緒です。ResetPasswordNotificationクラスにはtoMailメソッドが実装されているので、メール送信メソッドとして呼び出されます。

sendResetLinkの返却値に関して

ここで返却している値はステータスです。
実際に使われるコードは以下のような感じです。

return $status == Password::RESET_LINK_SENT
    ? app(SuccessfulPasswordResetLinkRequestResponse::class, ['status' => $status])
    : app(FailedPasswordResetLinkRequestResponse::class, ['status' => $status]);

まとめ

このようにしてPasswordBrokerは出来ています。
処理としてはうまく出来ているのがTokenRepositoryだなと感じますね。throttleとか、tokenをhasherを通してDBに入れることとか。

ではでは。