Laravel/認証周りのコード追う

LaravelPHP

\Auth::user()で自前のUserクラス(EloquentModelでもGenericUserでもないやつ)を取得できるようにしたかったのさ

  • Tymon氏のJWT認証追う
  • Laravelの標準認証ロジックも追う

JWT認証ミドルウェア定義部分

app/Http/Kernel.php

<?php
...

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,

        'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken',
        'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken',

    ];
  • Tymon\JWTAuth\Middleware\GetUserFromTokenがJWT認証ミドルウェア

JWT認証ミドルウェア

vendor/tymon/jwt-auth/src/Middleware/GetUserFromToken.php

<?php
...

class GetUserFromToken extends BaseMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, \Closure $next)
    {
        if (! $token = $this->auth->setRequest($request)->getToken()) {
            return $this->respond('tymon.jwt.absent', 'token_not_provided', 400);
        }

        try {
            $user = $this->auth->authenticate($token);
        } catch (TokenExpiredException $e) {
            return $this->respond('tymon.jwt.expired', 'token_expired', $e->getStatusCode(), [$e]);
        } catch (JWTException $e) {
            return $this->respond('tymon.jwt.invalid', 'token_invalid', $e->getStatusCode(), [$e]);
        }

        if (! $user) {
            return $this->respond('tymon.jwt.user_not_found', 'user_not_found', 404);
        }

        $this->events->fire('tymon.jwt.valid', $user);

        return $next($request);
    }
}
  • $this->authに処理を委譲しているようだ

    • $this->auth->setRequest($request)

      • Requestオブジェクトのセット
      • これをしないとJWTトークンをheaderに乗せてテストしてもうまくいかない
    • $user = $this->auth->authenticate($token);

      • 認証部分
  • $thisBaseMiddlewareを継承している

vendor/tymon/jwt-auth/src/Middleware/BaseMiddleware.php

<?php
...

    /**
     * @var \Tymon\JWTAuth\JWTAuth
     */
    protected $auth;
  • JWTAuthオブジェクトに処理を委譲している

認証ファサードクラス JWTAuth

  • Gang of Fourでいうところの「Facade」にあたるクラス

    • デザインパターンの話。これ自体はLaravelの「Facade機能」とは関係ない
  • JWTAuthクラスはtymon.jwt.authという名前でサービスコンテナにsingletonバインドされている

vendor/tymon/jwt-auth/src/Providers/JWTAuthServiceProvider.php

<?php
...

    /**
     * Bind some Interfaces and implementations.
     */
    protected function bootBindings()
    {
        $this->app->singleton('Tymon\JWTAuth\JWTAuth', function ($app) {
            return $app['tymon.jwt.auth'];
        });

...
  • このtymon.jwt.authという名前がLaravelのファサード機能で使用される

vendor/tymon/jwt-auth/src/Facades/JWTAuth.php

<?php
...

class JWTAuth extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'tymon.jwt.auth';
    }
}

config/app.php

<?php
...

    'aliases' => [
    ...
        'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth',
        'JWTFactory' => 'Tymon\JWTAuth\Facades\JWTFactory',
    ...
  • ここにおいて\JWTAuth::setRequest($request)とか\JWTAuth::authenticate($token)とか書けるようになる

  • JWTAuth@authenticateもまた、$this->authに処理を委譲している

vendor/tymon/jwt-auth/src/JWTAuth.php

<?php
...

    /**
     * Authenticate a user via a token.
     *
     * @param mixed $token
     *
     * @return mixed
     */
    public function authenticate($token = false)
    {
        $id = $this->getPayload($token)->get('sub');

        if (! $this->auth->byId($id)) {
            return false;
        }

        return $this->auth->user();
    }
  • AuthInterfaceとして抽象化されている何かのようだ
<?php
...

    /**
     * @var \Tymon\JWTAuth\Providers\Auth\AuthInterface
     */
    protected $auth;

インタフェースAuthInterface, 実装クラスIlluminateAuthAdapter

vendor/tymon/jwt-auth/src/Providers/Auth/AuthInterface.php

<?php

/*
 * This file is part of jwt-auth.
 *
 * (c) Sean Tymon <tymon148@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Tymon\JWTAuth\Providers\Auth;

interface AuthInterface
{
    /**
     * Check a user's credentials.
     *
     * @param  array  $credentials
     * @return bool
     */
    public function byCredentials(array $credentials = []);

    /**
     * Authenticate a user via the id.
     *
     * @param  mixed  $id
     * @return bool
     */
    public function byId($id);

    /**
     * Get the currently authenticated user.
     *
     * @return mixed
     */
    public function user();
}

vendor/tymon/jwt-auth/src/Providers/Auth/IlluminateAuthAdapter.php

<?php

/*
 * This file is part of jwt-auth.
 *
 * (c) Sean Tymon <tymon148@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Tymon\JWTAuth\Providers\Auth;

use Illuminate\Auth\AuthManager;

class IlluminateAuthAdapter implements AuthInterface
{
    /**
     * @var \Illuminate\Auth\AuthManager
     */
    protected $auth;

    /**
     * @param \Illuminate\Auth\AuthManager  $auth
     */
    public function __construct(AuthManager $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Check a user's credentials.
     *
     * @param  array  $credentials
     * @return bool
     */
    public function byCredentials(array $credentials = [])
    {
        return $this->auth->once($credentials);
    }

    /**
     * Authenticate a user via the id.
     *
     * @param  mixed  $id
     * @return bool
     */
    public function byId($id)
    {
        return $this->auth->onceUsingId($id);
    }

    /**
     * Get the currently authenticated user.
     *
     * @return mixed
     */
    public function user()
    {
        return $this->auth->user();
    }
}
  • IlluminateAuthAdapterは、Illuminate\Auth\AuthManagerJWTAuthから使用できるようにするAdapterクラス
  • Illuminate\Auth\AuthManagerは、Laravel標準の認証に関するFacadeクラスであり(GoF的な意味で)、LaravelのFacade機能の実体である
  • 結局、\JWTAuth::authenticate($token)\Auth::user()を返す
  • インタフェースと実装クラスのバインディングはJWTAuthServiceProviderに記述されている
<?php
...

        $this->app->singleton('Tymon\JWTAuth\Providers\Auth\AuthInterface', function ($app) {
            return $app['tymon.jwt.provider.auth'];
        });

...


    /**
     * Register the bindings for the Auth provider.
     */
    protected function registerAuthProvider()
    {
        $this->app->singleton('tymon.jwt.provider.auth', function ($app) {
            return $this->getConfigInstance($this->config('providers.auth'));
        });
    }

$this->config('providers.auth')は、config/jwt.phpのproviders->authを読みに行く人

<?php

    'providers' => [
    ...

        /*
        |--------------------------------------------------------------------------
        | Authentication Provider
        |--------------------------------------------------------------------------
        |
        | Specify the provider that is used to authenticate users.
        |
        */

        'auth' => 'Tymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter',
  • 要するに、Tymon\JWTAuth\Providers\Auth\AuthInterfaceTymon\JWTAuth\Providers\Auth\IlluminateAuthAdapterとして解決される

\JWTAuth::authenticate($token)で自前のUserクラスのオブジェクトが返ってくるようにしたい

  • \Auth::user()で自前のUserクラスのオブジェクトが変えるようにすれば良い

\Auth::user()で自前のUserクラスのオブジェクトが返ってくるようにしたい

  • \Auth::user()は、app()->make('auth')->user()である
  • app()->make('auth')は、Illuminate\Auth\AuthManagerのインスタンスを返す
  • Illuminate\Auth\AuthManager@userは、Illuminate\Contracts\Auth\Guard@userに処理を委譲する
    /**
     * Dynamically call the default driver instance.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->guard()->{$method}(...$parameters);
    }
  • デフォルトのガードはweb、driverはsession、providerはusers

config/auth.php

<?php
...
    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],
...

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
        ],
    ],
...

    'providers' => [
//        'users' => [
//            'driver' => 'eloquent',
//            'model' => App\User::class,
//        ],

         'users' => [
             'driver' => 'dto',
         ],
    ],
  • (Eloquentモデル等ではなく)自前のUserオブジェクトを取得するためには、自前のUserProviderを書く必要がある

    • dtoってやつ

自前のUserProvider書く

<?php
declare(strict_types=1);

namespace App\Domain\User;

use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Auth\DatabaseUserProvider;
use App\Infrastructure\Db\UserDao;
use App\Domain\User\User;

/**
 * 認証情報からApp\Domain\User\Userを得るなどする
 */
class DtoUserProvider extends DatabaseUserProvider implements UserProvider
{
    /** @var UserDao */
    private $userDao;

    public function __construct(UserDao $userDao)
    {
        parent::__construct(
            app()->make('db')->connection(),
            app()->make('hash'),
            UserDao::TABLE_NAME
        );

        $this->userDao = $userDao;
    }

    /**
     * Retrieve a user by their unique identifier.
     *
     * @override
     * @param  mixed  $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveById($identifier)
    {
        return $this->userDao->find($identifier);
    }

    /**
     * Retrieve a user by the given credentials.
     *
     * @override
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     *
     * @todo クエリ1回で済ます
     */
    public function retrieveByCredentials(array $credentials)
    {
        // 雑にgenericUserを得て、
        // idから再度Userを得る
        //
        // 2回クエリが実行されるのが良くない
        $genericUser = parent::retrieveByCredentials($credentials);
        return is_null($genericUser) ? null : $this->userDao->find($genericUser->id);
    }
}
  • インフラ層のDAO(UserDao)からドメイン層のDTO(User)を得る世界観

    • DAOがドメイン層の型を意識してしまっている
    • 本当はDAOの返却値の型とUserとをマッピングするクラスがないと駄目なんだろうなあ
  • DatabaseUserProviderを雑に継承して差分だけ書いている
  • これをdtoという名前でAuthManagerオブジェクト(シングルトン)に登録する

app/Providers/AuthServiceProvider.php

<?php
...

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerUserProvider();
    }

    /**
     * 自前DTO版UserProviderの登録
     */
    protected function registerUserProvider()
    {
        $this->app->make('auth')->provider(
            'dto',
            function ($app) {
                return $app->make(DtoUserProvider::class);
            }
        );
    }

自作UserProviderが使用される流れ

  • AuthManager@providerで登録すると、CreatesUserProvidersトレイトのcustomProviderCreators[]メンバに登録される

Illuminate\Auth\AuthManager.php

<?php
...
class AuthManager implements FactoryContract
{
    use CreatesUserProviders;

...

    /**
     * Register a custom provider creator Closure.
     *
     * @param  string  $name
     * @param  \Closure  $callback
     * @return $this
     */
    public function provider($name, Closure $callback)
    {
        $this->customProviderCreators[$name] = $callback;

        return $this;
    }
  • デフォルトのwebガードを構築する際にsessionドライバーを構築する
  • sessionドライバーはAuthManager@createSessionDriverで構築される
  • その中でCreatesUserProvidersトレイトのcreateUserProviderが呼ばれ、UserProvider実装クラスのオブジェクトが構築される
<?php

    /**
     * Create a session based authentication guard.
     *
     * @param  string  $name
     * @param  array  $config
     * @return \Illuminate\Auth\SessionGuard
     */
    public function createSessionDriver($name, $config)
    {
        $provider = $this->createUserProvider($config['provider'] ?? null);

        $guard = new SessionGuard($name, $provider, $this->app['session.store']);

        // When using the remember me functionality of the authentication services we
        // will need to be set the encryption instance of the guard, which allows
        // secure, encrypted cookie values to get generated for those cookies.
        if (method_exists($guard, 'setCookieJar')) {
            $guard->setCookieJar($this->app['cookie']);
        }

        if (method_exists($guard, 'setDispatcher')) {
            $guard->setDispatcher($this->app['events']);
        }

        if (method_exists($guard, 'setRequest')) {
            $guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
        }

        return $guard;
    }

Illuminate\Auth\CreateUserProviders.php

<?php

...

    /**
     * Create the user provider implementation for the driver.
     *
     * @param  string|null  $provider
     * @return \Illuminate\Contracts\Auth\UserProvider|null
     *
     * @throws \InvalidArgumentException
     */
    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
            );
        }

        switch ($driver) {
            case 'database':
                return $this->createDatabaseProvider($config);
            case 'eloquent':
                return $this->createEloquentProvider($config);
            default:
                throw new InvalidArgumentException(
                    "Authentication user provider [{$driver}] is not defined."
                );
        }
    }
            return call_user_func(
                $this->customProviderCreators[$driver], $this->app, $config
            );
  • \Auth::user()で、自前のUserオブジェクトが得られるようになる。めでたし。