Laravel : Sanctumってどうなのよ?

Date:

Share post:

Laravel 9.x を使い始めたので、API認証に関しても従来のsessionガード方式からSanctumに切り替えてみようかと思ったのですが、やってみると意外にうまく使えなかったので、その辺に関して書きたいと思います。

従来の認証

まず、従来のsessionガード方式に関しておさらいしておきたいと思います。

認証の設定ファイルである「config/auth.php」に以下のような記述を追加します。

'guards' => [
    'user' => [
        'driver' => 'session',
        'provider' => 'user',
    ],
    'admin' => [
        'driver' => 'session',
        'provider' => 'admin',
    ],
],

'providers' => [
    'user' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],
    'admin' => [
        'driver' => 'eloquent',
        'model' => App\Models\Admin::class,
    ],
],

上記のような設定を行うことで認証情報(アカウント、パスワード等)を「model」で指定したEloquent(つまりはテーブル)の情報と照合するようになり、成功した場合は認証状態がセッションに記録されるようになります。

照合の仕方は以下のように対象となるguardを指定して行います。

if (Auth::guard('user')->attempt($credentials)) {
    // 認証成功時の処理
}

$credentialsは認証に必要なデータ(アカウント、パスワード等)から構成される連想配列で、キーは比較対象となるテーブルのカラム名になります。

ルーティングにおいては「routes/api.php」でmiddlewareとして対象となるguardを指定して認証状態を確認するように設定します。

Route::get('パス', XXXController::class)->middleware('auth:user');

上記定義を行っておくだけで認証していない状態でのアクセスはエラーになるようになります。
便利ですね。

Sanctumを使用した認証

Santcumを使用して認証を行う場合、まずはミドルウェア「EnsureFrontendRequestsAreStateful」を有効にする必要があります。
このミドルウェアに関してはLaravel9.xを直接インストールした場合は「app/Http/Kernel.php」内に該当する記述がコメントアウトされた形で既に存在しますので、このコメントを有効にするのみです。

        'api' => [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, // これ
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

ちなみに上記で有効にしたミドルウェア内には以下のような処理があります。

public function handle($request, $next)
{
    $this->configureSecureCookieSessions();

    return (new Pipeline(app()))->send($request)->through(static::fromFrontend($request) ? [
        function ($request, $next) {
            $request->attributes->set('sanctum', true);

            return $next($request);
        },
        config('sanctum.middleware.encrypt_cookies', \Illuminate\Cookie\Middleware\EncryptCookies::class),
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        config('sanctum.middleware.verify_csrf_token', \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class),
    ] : [])->then(function ($request) use ($next) {
        return $next($request);
    });
}

行が折り返して見難いかと思いますが、「fromFrontend」なるメソッドが真を返した場合はミドルウェアをいくつか実行するように設定していますが、これらはセッションやCSRFに関係するものです。
「app/Http/Kernel.php」内の $middlewareGroups[‘web’] では直接設定されているようなミドルウェアがここで間接的に設定されている形になっています。

なお、fromFrontendメソッド内では、リクエストヘッダで指定された「referer」もしくは「origin」と config(‘sanctum.stateful’) で指定されているドメインを比較して一致していた場合は真を返します。
「config/sanctum.php」では.env内で「SANCTUM_STATEFUL_DOMAINS」として指定されていたドメインをconfig(‘sanctum.stateful’) に追加するようになっています。
つまり、認証を有効にするドメインを.envの「SANCTUM_STATEFUL_DOMAINS」で指定しておかないと認証が有効にならないと言うことです。

上記でSanctumにおける認証関連の処理の準備ができた訳ですが、では「config/auth.php」内の記述や認証の成否(Auth::attempt()の実行)に関してはどうかと言うと、こちらは従来のsessionガード方式と同等の記述を行っておく必要があるようです。
つまりSanctumにおいてもAPI認証方法は実質的にはsessionガード方式と同じで、前述したようにセッション関連のミドルウェアの利用設定や認証済みかどうかのチェックのみをSanctum独自に行うようです。この辺からSanctumの存在意義に暗雲が立ち込め始めます(個人的感想)。

では、実際にSanctumを使用して認証が必要なルートの定義を書いてみましょう。

Route::get('パス', XXXController::class)->middleware('auth:sanctum');

Sanctumを使って認証チェックを行う場合は上記のようにauthの後ろにguard名ではなく「sanctum」と固定的に書くようです。
そうなると疑問が。
先ほどから例示してきたように、複数のguard(userとadmin等)を採用したい場合はどうなるのでしょう?

Sanctum内の認証に関する処理内容

頑張ってSanctum内の処理を追っていくと以下の処理が発見できます。

「vendor/laravel/sanctum/src/Guard.php」

public function __invoke(Request $request)
{
    foreach (Arr::wrap(config('sanctum.guard', 'web')) as $guard) {
        if ($user = $this->auth->guard($guard)->user()) {
            return $this->supportsTokens($user)
                ? $user->withAccessToken(new TransientToken)
                : $user;
        }
    }
    ...

これを見ると config(‘sanctum.guard’) で指定された(指定されなければ「web」)guardに対して認証済みかどうかをチェックしているようです。foreach文なので複数のguardに対応していそうですが…

「config/sanctum.php」内を見てみるとguardの指定はやはり配列形式になっており、複数のguardを指定できるようです。
例えば以下のように。

'guard' => ['user', 'admin'],

でも、これって middleware(‘auth:sanctum’) と指定されたルートでは必ず両方の認証をチェックし、どちらかでも認証済みであればOKにしているってことですよね?
user用、admin用と言う使い分けができておらず、一般ユーザーで認証済みであれば管理者機能まで使えてしまうと言うことになってしまいます。

この辺をネットで調べてみると、以下のような用法が出てきました。

'guards' => [
    'user_sanctum' => [
        'driver' => 'sanctum',
        'provider' => 'user',
    ],
    'admin_sanctum' => [
        'driver' => 'sanctum',
        'provider' => 'admin',
    ],
],

'providers' => [
    'user' => [
        'driver' => 'eloquent',
        'model' => App\Models\TUser::class,
    ],
    'admin' => [
        'driver' => 'eloquent',
        'model' => App\Models\TAdmin::class,
    ],
],

上記のように定義しておいて、ルート定義では以下のように指定します。

Route::get('パス1', XXXController::class)->middleware('auth:user_sanctum');
Route::get('パス2', YYYController::class)->middleware('auth:admin_sanctum');

パッと見では筋が通ってそうに見えるのですが、このケースでも実際に認証チェックを行っているロジックは先に示した「vendor/laravel/sanctum/src/Guard.php」でした。
つまり認証に使用されるguardは結局は config(‘sanctum.guard’) から取得できるものであって、「user_sanctum」「admin_sanctum」の両方を有効にしたい場合は「config/sanctum.php」内のguardの配列に両方を書いておく必要があります。

'guard' => ['user_sanctum', 'admin_sanctum'],

ルートの書式では両者を区別できているような印象になっていますが、結果的には何も変わっていないようです。

所感

Sanctumは「SPA、モバイルアプリケーション、およびシンプルなトークンベースのAPIに軽い認証システムを提供するもの」らしいので、主目的が認証であることは間違いないかと思うのですが、なぜ複数guardの考慮をしていないんでしょうね?

トークン方式を使えば良いかとも思いましたが、公式ページの注意書きに「独自のファーストパーティSPAを認証するためにAPIトークンを使用しないでください。代わりに、Sanctumの組み込みのSPA認証機能を使用してください。」と書いてあって、理由は不明ですが自社製SPAのためのAPIではトークンを使うことは邪道or禁止っぽく、結局はSPA認証(クッキーベースのセッション認証サービス)に戻ってきました。

先に少し紹介しましたが、ネット上でもSanctumで複数認証を実現する方法が議論されていたりするので、疑問に思っているのは私一人ではなさそうです。
APIにおける認証は1つでよく、今回例示したように一般ユーザーと管理者のような使い分けを行う場合はロール(認可)で対応すべきと言う意見もあるようですが、個人的には違和感ありです。

そもそも「config/sanctum.php」内のguard設定は配列形式であり、複数のguardを使う想定はあるのに、その使い分けは考慮していないと言うのも不思議です。

色々と疑問はありますが、少なくとも現状は認証については従来通りsessionガード方式を使うしかなさそうです。
ではSanctumは不要かというとそうでもなく、CSRFやCORS関連の処理を行ってくれるようなので、当面はそちらの用途で使用しておいて、将来的に認証機能に何か改善があることを期待したいと思います。

Related articles

我流Flutter学習ステップ(6)スマホでの動作...

本シリーズ(?)の最初の投稿で書いた...

その「平均値」に意味はあるのか?

最近「平均」に関して思うところがあっ...

遅まきながら、Vagrant(Virtualbox...

前回は、pumaを常時起動のユーザーサ...