カテゴリー: BackEnd

【Laravel】認証を自作して学ぶguardとmiddleware

はじめに

Laravelでの独自の認証処理の実装を通して、認証関連のmiddlewareやguardについてまとめました。

認証処理の仕様

今回、実装する簡易な認証処理の仕様は、以下の通りです。APIでの使用を想定しています。

  • ヘッダーのAuthenticationに設定したBearerトークンの値が、予め決めた文字列と一致すること
  • ヘッダーのuuid設定した値が、予めDBに登録してあるユーザのUUIDと一致すること

独自の認証処理の実装

独自の認証処理では、以下の項目を実装します。順番に内容を見ていきます。

  • Model
  • UserProvider
  • Guard
  • Middleware
  • Handlerとその他の設定

Modelの実装

認証処理に使うモデルは Illuminate\Foundation\Auth\User を継承している必要があります。今回実装する認証処理の仕様では、 getAuthIdentifierName() のみ実装すれば十分です。

<?php

namespace App\Domain\Model; // namespaceはお好みで

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */    protected $fillable = [
        'uuid',
    ];

    /**
     * Get the name of the unique identifier for the user.
     *
     * @see \Illuminate\Contracts\Auth\Authenticatable:: getAuthIdentifierName
     * @return string
     */    public function getAuthIdentifierName()
    {
        return 'uuid';
    }
}

UserProviderの実装

UserProvider では、ユーザーの識別方法を実装します。今回実装する認証処理の仕様では、リクエストヘッダーに毎回 UUID を含めるので、 retrieveById($identifier) のみ使用します。

<?php

namespace App\Providers;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use App\Domain\Model\User;

class ApiUserProvider implements UserProvider
{
    private User $_model;

    public function __construct(User $model)
    {
        $this->_model = $model;
    }

    /**
     * Retrieve a user by their unique identifier.
     *
     * @see \Illuminate\Contracts\Auth\UserProvider::retrieveById
     * @param  mixed  $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */    public function retrieveById($identifier)
    {
        return $this->_model->where($this->_model->getAuthIdentifierName(), $identifier)->first();
    }

    /**
     * Retrieve a user by their unique identifier and "remember me" token.
     *
     * @see \Illuminate\Contracts\Auth\UserProvider::retrieveByToken
     * @param  mixed  $identifier
     * @param  string  $token
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */    public function retrieveByToken($identifier, $token)
    {
        return null;
    }

    /**
     * Update the "remember me" token for the given user in storage.
     *
     * @see \Illuminate\Contracts\Auth\UserProvider::updateRememberToken
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  string  $token
     * @return void
     */    public function updateRememberToken(Authenticatable $user, $token)
    {
    }

    /**
     * Retrieve a user by the given credentials.
     *
     * @see \Illuminate\Contracts\Auth\UserProvider::retrieveByCredentials
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */    public function retrieveByCredentials(array $credentials)
    {
        return null;
    }

    /**
     * Validate a user against the given credentials.
     *
     * @see \Illuminate\Contracts\Auth\UserProvider::validateCredentials
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        return false;
    }
}

Guardの実装

Guard では、認証に直接関連する処理を実装します。今回実装する認証処理の仕様では、まず、 Bearerトークンが事前に決めた文字列と一致しているか確認し、一致していなければ、問答無用で認証エラーとしたいので、まずそれを確認します。その後、uuidヘッダーの値でユーザーを識別します。

<?php


namespace App\Guards;


use Illuminate\Auth\Events\Authenticated;
use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Auth\GuardHelpers as GuardHelpers;

class BearerGuard implements Guard
{
    use GuardHelpers;

    protected string $_name;
    protected UserProvider $_provider;
    protected ?Request $_request;
    protected ?Authenticatable $_user;

    /**
     * Create a new authentication guard.
     *
     * @param string $name
     * @param UserProvider $provider
     * @param Request $request
     */    public function __construct(string $name, UserProvider $provider, Request $request = null)
    {
        $this->_name = $name;
        $this->_request = $request;
        $this->_provider = $provider;
        $this->_user = null;
    }

    /**
     * Determine if the current user is authenticated.
     *
     * @return bool
     */    public function check()
    {
        return $this->checkAccessToken() && !is_null($this->user());
    }

    /**
     * Determine if the current user is a guest.
     *
     * @return bool
     */    public function guest()
    {
        return !$this->check();
    }

    /**
     * Get the currently authenticated user.
     *
     * @return Authenticatable|null
     */    public function user()
    {
        if (!is_null($this->_user)) {
            return $this->_user;
        }

        // uuidヘッダの内容でユーザーを識別
        $cid = $this->_request->header('uuid', '');
        if ($this->_user = $this->_provider->retrieveById($cid)) {
            $this->fireAuthenticatedEvent($this->_user);
        }

        return $this->_user;
    }

    /**
     * Get the ID for the currently authenticated user.
     *
     * @return int|string|null
     */    public function id()
    {
        if ($user = $this->user()) {
            return $this->user()->getAuthIdentifier();
        }
        return null;
    }

    /**
     * Validate a user's credentials.
     *
     * @param  array  $credentials
     * @return bool
     */    public function validate(array $credentials = [])
    {
        return false;
    }

    /**
     * Set the current user.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @return void
     */    public function setUser(Authenticatable $user)
    {
        $this->_user = $user;
    }

    /**
     * Fire the authenticated event if the dispatcher is set.
     *
     * @param Authenticatable $user
     * @return void
     */    protected function fireAuthenticatedEvent($user)
    {
        if (isset($this->events)) {
            $this->events->dispatch(new Authenticated($this->_name, $user));
        }
    }

    private function checkAccessToken(): bool
    {
        return $this->_request->bearerToken() == config('auth.api_access_token');
    }
}

checkAccessToken() の中で、 Bearerトークンの値と、 .env に設定した値が一致するか、確認しています。

ユーザーの識別は、 user() の中で行っており、uuid ヘッダーの値と一致するユーザーを、 UserProvider 経由で取得しています。

なお、 check() 、 guest() 、 user() などは Authファサードからアクセスできます。

// ログイン状態か
Auth::check();

// 未ログイン状態か
Auth::guest();

// ログインしているユーザを取得
$user = Auth::user();

Middlewareの実装

認証エラーになった場合、そのままではリダイレクトされてしまうので、JSONを返却できるように、 Middleware で制御します。

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Auth\Factory as Auth;
use Illuminate\Http\Request;

class AuthenticateForApi
{
    /** @var Auth The authentication factory instance. */    protected Auth $_auth;

    /**
     * Create a new middleware instance.
     *
     * @param Auth $auth
     * @return void
     */    public function __construct(Auth $auth)
    {
        $this->_auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  Request $request
     * @param  Closure $next
     * @param  string[] ...$guards
     * @return mixed
     */    public function handle($request, Closure $next, ...$guards)
    {
        $this->authenticate($guards);

        return $next($request);
    }

    /**
     * Determine if the user is logged in to any of the given guards.
     *
     * @param  array $guards
     * @return void
     */    protected function authenticate(array $guards)
    {
        if (empty($guards)) {
            $guards = [null];
        }

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

        abort(401);
    }
}

authenticate(array $guards) のループの中で guard を一つずつチェックし、認証でたら、 shouldUse($guard) で、その guard を AuthManager の $defaultProvider にセットし、$userProvider にユーザーの解決方法をクロージャの形式でセットします。

Handlerの編集

認証エラーになった場合、そのままではリダイレクトされてしまうので、 JSON を返却するように修正します。

<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *
     * @var array
     */    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    /**
     * Report or log an exception.
     *
     * @param  \Throwable  $exception
     * @return void
     *
     * @throws \Exception
     */    public function report(Throwable $exception)
    {
        parent::report($exception);
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Throwable  $exception
     * @return \Symfony\Component\HttpFoundation\Response
     *
     * @throws \Throwable
     */    public function render($request, Throwable $exception)
    {
        // ↓↓↓ここから追記
        if ($request->is('ajax/*') || $request->is('api/*') || $request->ajax()) {
            return $this->createApiErrorResponse($exception);
        }
        // ここまで追記↑↑↑
        return parent::render($request, $exception);
    }
    // ↓↓↓ここから追記

    private function createApiErrorResponse(Throwable $exception)
    {
        if ($this->isHttpException($exception)) {
            $status = $exception->getStatusCode();
            switch ($status) {
                case 400:
                    $errorMsg = 'Bad request.';
                    break;
                case 401:
                    $errorMsg = 'Unauthorized.';
                    break;
                case 404:
                    $errorMsg = 'Not found.';
                    break;
                default:
                    $status = 400;
                    $errorMsg = $exception->getMessage();
            }
        } else {
            $status = 500;
            $errorMsg = $exception->getMessage();
        }
        $resultCode = 0;
        return response()->json([
            'status' => $resultCode,
            'message' => $errorMsg,
        ], $status);
    }
    // ここまで追記↑↑↑
}

各種設定

auth.php に指定するために、AuthServiceProvider.php で、作成した guard を登録します。

<?php

namespace App\Providers;

use App\Guards\BearerGuard; // 追記
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth; // 追記
use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */    protected $policies = [
        // 'App\Model' => 'App\Policies\ModelPolicy',
    ];

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

        // ↓↓↓ここから追記
        Auth::extend('bearer', function ($app) {
            $userProvider = $app->make(ApiUserProvider::class);
            $request = $app->make('request');
            return new BearerGuard('bearer', $userProvider, $request);
        });
        // ここまで追記↑↑↑
    }
}

作成した Middleware を kernel.php に定義します。

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */    protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Fruitcake\Cors\HandleCors::class,
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    /**
     * 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,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        // ↓↓↓ここから追記
        'auth.api' => \App\Http\Middleware\AuthenticateForApi::class,
        // ここまで追記↑↑↑
    ];
}

auth.php で、作成した認証処理を使用するように設定します。

<?php

return [
    // 以下抜粋
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'admin_users',
        ],

        'api' => [
            'driver' => 'bearer',
        ],
    ],
];

作成した認証処理を使う

作成した認証処理を使うには、 api.php 内で以下のように記述します。

<?php

use Illuminate\Support\Facades\Route;
// ログイン時
Route::middleware('auth.api:api')->get('/users', 'UsersController@authenticated');

// 未ログイン時
Route::middleware('guest.api:api')->get('/users', 'UsersController@noauthenticated');
});

さいごに

APIの認証には、 jwt-auth など、すでに様々なライブラリが用意されているので、独自の認証処理を実装することは少ないかもしれません。ただ、今回のように簡易な認証で良い場合は、 Laravel では簡単に実装することができます。

おすすめ書籍

 

Hiroki Ono

シェア
執筆者:
Hiroki Ono
タグ: phplaravel

最近の投稿

Goの抽象構文木でコードを解析する

はじめに Goでアプリケーショ…

5日 前

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

1か月 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

2か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前