2024-06-26

Laravelの認証の仕組み

Laravelの認証周りのコードリーディングメモ。

↓こんな感じなパターンを見ていく

Route::get('/resources', function () {
    $user = Auth::user();
    // ...
})->middleware('auth');

Route::post('/login', function ($request) {
    if (Auth::attempt($request->all()) {
        // ...
    }
});

認証チェック

Illuminate\Auth\Middleware\Authenticate ミドルウェアでチェックしています。

public function handle($request, Closure $next, ...$guards)
{
    $this->authenticate($request, $guards);

    return $next($request);
}

protected function authenticate($request, array $guards)
{
    if (empty($guards)) {
        $guards = [null];
    }

    foreach ($guards as $guard) {
        if ($this->auth->guard($guard)->check()) {
            return $this->auth->shouldUse($guard);
        }
    }

    $this->unauthenticated($request, $guards);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/Middleware/Authenticate.php#L60-L65 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/Middleware/Authenticate.php#L76-L89

Illuminate\Auth\AuthManager::guard($guard)->check() でチェックして認証が通っていなければ unauthenticated() メソッド経由で AuthenticationException をスローします。

guard() は引数がnullの場合は getDefaultDriver() でdriver名を取得して resolve() した値を取得します。

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

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

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/AuthManager.php#L66-L71

resolve() はconfigから取得してdriverを取得して各driverメソッドを呼び出します。

protected function resolve($name)
{
    $config = $this->getConfig($name);

    if (is_null($config)) {
        throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
    }

    if (isset($this->customCreators[$config['driver']])) {
        return $this->callCustomCreator($name, $config);
    }

    $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

    if (method_exists($this, $driverMethod)) {
        return $this->{$driverMethod}($name, $config);
    }

    throw new InvalidArgumentException(
        "Auth driver [{$config['driver']}] for guard [{$name}] is not defined."
    );
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/AuthManager.php#L81-L102

デフォルトの値は session が利用されるので createSessionDriver()Illuminate\Auth\SessionGuard を取得します。

SessionGuard::check()user() がnullかどうかで判定しています。

public function check()
{
    return ! is_null($this->user());
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/GuardHelpers.php#L54-L57

user() ではセッションからユーザIDを取得し、provider経由でユーザのModelインスタンスを取得します。

public function user()
{
    if ($this->loggedOut) {
        return;
    }

    if (! is_null($this->user)) {
        return $this->user;
    }

    $id = $this->session->get($this->getName());

    if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
        $this->fireAuthenticatedEvent($this->user);
    }
 
    // ...(省略)...   

    return $this->user;
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/SessionGuard.php#L151-L187

providerは AuthManager::createUserProvider() で生成しています。

public function createUserProvider($provider = null)
{
    if (is_null($config = $this->getProviderConfiguration($provider))) {
        return;
    }

    if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
        return call_user_func(
            $this->customProviderCreators[$driver], $this->app, $config
        );
    }

    return match ($driver) {
        'database' => $this->createDatabaseProvider($config),
        'eloquent' => $this->createEloquentProvider($config),
        default => throw new InvalidArgumentException(
            "Authentication user provider [{$driver}] is not defined."
        ),
    };
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/AuthManager.php#L125 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/CreatesUserProviders.php#L24-L43

getProviderConfiguration()auth.providers.{provider_name} のconfigを取得し、driverがeloquentであれば createEloquentProvider() を呼び出し、 EloquentUserProvider を返します。

protected function createEloquentProvider($config)
{
    return new EloquentUserProvider($this->app['hash'], $config['model']);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/CreatesUserProviders.php#L77-L80

EloquentUserProvider::retrieveById()createModel() でconfigで指定したモデルをインスタンス化し、 Model::newModelQuery() 経由でクエリを叩き、DBからレコードを取得します。

public function retrieveById($identifier)
{
    $model = $this->createModel();

    return $this->newModelQuery($model)
                ->where($model->getAuthIdentifierName(), $identifier)
                ->first();
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/EloquentUserProvider.php#L53-L60

ユーザ情報取得と認証

Illuminate\Auth\AuthManageruser()attempt()__call でguardに処理を移譲されます。

public function __call($method, $parameters)
{
    return $this->guard()->{$method}(...$parameters);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/AuthManager.php#L339-L342

user() は上記の処理でAuthenticateミドルウェアで認証チェック済みの場合は、コントローラで処理する際はすでにモデルがセットされている状態になります。

attempt()$credentials のarrayデータを受け取って providerの retriveByCredentials() でデータを取得し、パスワードが正しいかどうかを hasValidCredentials で検証します。

public function attempt(array $credentials = [], $remember = false)
{
    $this->fireAttemptEvent($credentials, $remember);

    $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

    if ($this->hasValidCredentials($user, $credentials)) {
        $this->rehashPasswordIfRequired($user, $credentials);

        $this->login($user, $remember);

        return true;
    }

    $this->fireFailedEvent($user, $credentials);

    return false;
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/SessionGuard.php#L387-L410

retriveByCredentials() ではパスワード以外の$credentialsデータ(例えばemail)を抽出し、それらを使ってDBを検索します。

public function retrieveByCredentials(array $credentials)
{
    $credentials = array_filter(
        $credentials,
        fn ($key) => ! str_contains($key, 'password'),
        ARRAY_FILTER_USE_KEY
    );

    if (empty($credentials)) {
        return;
    }

    $query = $this->newModelQuery();

    foreach ($credentials as $key => $value) {
        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();
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/EloquentUserProvider.php#L112-L140

hasValidCredentials() ではproviderの validateCredentials() でパスワードの値を検証しています。

protected function hasValidCredentials($user, $credentials)
{
    return $this->timebox->call(function ($timebox) use ($user, $credentials) {
        $validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials);

        if ($validated) {
            $timebox->returnEarly();

            $this->fireValidatedEvent($user);
        }

        return $validated;
    }, 200 * 1000);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/SessionGuard.php#L449-L462

ちなみに timebox はバリデーション失敗時に早くレスポンスを返しすぎて攻撃者にヒントを与えないために指定の時間まで待ってレスポンスを返す仕組みです。

public function call(callable $callback, int $microseconds)
{
    $exception = null;

    $start = microtime(true);

    try {
        $result = $callback($this);
    } catch (Throwable $caught) {
        $exception = $caught;
    }

    $remainder = intval($microseconds - ((microtime(true) - $start) * 1000000));

    if (! $this->earlyReturn && $remainder > 0) {
        $this->usleep($remainder);
    }

    if ($exception) {
        throw $exception;
    }

    return $result;
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Support/Timebox.php#L25-L48

public function validateCredentials(UserContract $user, array $credentials)
{
    if (is_null($plain = $credentials['password'])) {
        return false;
    }

    return $this->hasher->check($plain, $user->getAuthPassword());
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/EloquentUserProvider.php#L149-L156

SessionGuard::login()updateSession() でセッションを更新し、setUser() でインスタンス変数にユーザモデルなどをセットします。

public function login(AuthenticatableContract $user, $remember = false)
{
    $this->updateSession($user->getAuthIdentifier());

    if ($remember) {
        $this->ensureRememberTokenIsSet($user);

        $this->queueRecallerCookie($user);
    }

    $this->fireLoginEvent($user, $remember);

    $this->setUser($user);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/SessionGuard.php#L521-L540

updateSession() はセッションにユーザIDをセットして、migrate() によってセッションIDを洗替えします。

protected function updateSession($id)
{
    $this->session->put($this->getName(), $id);

    $this->session->migrate(true);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Auth/SessionGuard.php#L548-L553

このエントリーをはてなブックマークに追加