tohokuaikiのチラシの裏

技術的ネタとか。

Laravel11でSanctumをやってみようと思ったけど、セッションログインでいいじゃんって

Inertia.js使ってReactで管理画面の開発してると、「CSRFとかXSSとかどないやねん?」って感じになる。Sanctumを使ったSPA認証ってのがあるみたいだ。

laravel.com

2つ認証にたいするアプローチがあって、

  1. APIトークンを付与してそのトークンのやり取りで認証する。トークンの期限をサーバ側で決める。トークンはHTTPヘッダに入れる。(Bearer認証)
  2. セッションCOOKIEトークンを付与する。

というもの。前者はコマンドラインからAPIを叩くときに使って、後者はブラウザから使う。「ログインしてるんだから、いーじゃん。」と思ったら、やっぱりそうだった。どうも従来のログインを使わないでSPAでログインしたりしたい時に使うっぽい。だから、ログインの実装には別のパッケージが必要で、ログインを実装するには、「手動で実装するか、Laravel Fortify のようなヘッドレスの認証パッケージを使ってください」と公式ページにもある。Qiitaに実装した人の記事あるけど、めんどくさそうですね。

ということで、セッションログインでいいじゃんって。

セッションAPIにする

ルーティング

middleware('auth')をルーティングにつける。

<?php
Route::apiResource('users', UserController::class)->middleware('auth');

bootstrap/app.php のwithMiddlewareで、->api(append:[]) する。

ミドルウェアの登録

<?php
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->web(append: [
            \App\Http\Middleware\HandleInertiaRequests::class,
            \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
        ])->api(append: [
            \Illuminate\Cookie\Middleware\EncryptCookies::class,
        ]);

vendor/laravel/framework/src/Illuminate/Foundation/Configuration/Middleware.php を見ると、

 \Illuminate\Cookie\Middleware\EncryptCookies::class,
 \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
 \Illuminate\Session\Middleware\StartSession::class,
 \Illuminate\View\Middleware\ShareErrorsFromSession::class,
 \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
 \Illuminate\Routing\Middleware\SubstituteBindings::class,

の6つがwebミドルウェアグループに登録してあったけど、1つだけでなんとかなった。

あんまり簡単だったんで、Sanctumを導入して「あるべきAPI」の形にしてみる。

といっても、トークンじゃなくてSPA認証で。

サーバ(Laravel側)の設定

Configuring Your First-Party Domains

ドメインを一致させておけと。当たり前ですね。

Sanctum Middleware

ミドルウェアを変更する。 $middleware->statefulApi(); を追加。

<?php
->withMiddleware(function (Middleware $middleware) {
         $middleware->statefulApi();

CORS and Cookies

ドメインじゃないからこれもパス。

ルーティング

こんな感じでmiddleware('auth:sanctum')をルーティングにつける。

<?php
Route::apiResource('users', UserController::class)->middleware('auth:sanctum');

クライアント(React側)の設定

ログイン後に、TOKENをもらってCOOKIEに入れておく。…というより、Set-Cookieヘッダによりリクエスト送ったら自動でCOOKIEがセットされるので、そのCOOKIEをHTTPヘッダに乗せてやることでAuthorizationが行われるということ。

TOKENをもらうフェーズ

http://localhost:8000/sanctum/csrf-cookie にGETでfetchを飛ばすとTokenを発行してくれる。XSRF-TOKENという名前でCOOKIEがセットされているはず。

自分の場合、Inertia.jsで管理画面を作ってるこのコードだと、resources/js/Pages/Admin/Default.tsx がデフォルトラッパーになるのでここでAjaxを飛ばして取得する。飛ばしさえすればあとはSet-Cookieレスポンスヘッダが返ってくるだけなので、何もしなくても大丈夫。Baseloadingは適当なローダー。

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { GlobalConstantsProvider, GlobalConstantsType } from '@/Contexts/GlobalConstants';
import { Head } from '@inertiajs/react';
import BaseLoading from '@/Components/Baseloading';
import { useState } from 'react';

const Default = ({ admin_path }: {
    admin_path: string;
}) => {

    const [loading, setLoading] = useState<boolean>(true);

    (async () => {
        await fetch(route('sanctum.csrf-cookie'))
        setLoading(false)
    })()

    const globalConstants: GlobalConstantsType = {
        ADMIN_PATH: admin_path
    }

    const wrapperStyle = {
        width: "100%",
        height: "100%",
        overflow: "hidden",
    }

    return (
        <GlobalConstantsProvider defaultValue={globalConstants}>
            {loading ? <BaseLoading active={true} wrapper={wrapperStyle} fadeSpeed={100}></BaseLoading> :
                <>
                    <Head title="CoreUI Adminstration Panel Sample" />
                    <AuthenticatedLayout />
                </>
            }
        </GlobalConstantsProvider>
    );
}
export default Default

TOKENを使うフェーズ

RequestヘッダにX-XSRF-TOKENを含める。その値はCOOKIEにあるXSRF-TOKEN。axiosを使ってインターセプトしてやるとよい。

    axios.interceptors.request.use(config => {
        config.headers!['X-XSRF-TOKEN'] = cookies['XSRF-TOKEN']
        console.log(cookies['XSRF-TOKEN']);
        return config;
    }, error => {
        return Promise.reject(error)
    })

…が、うまくいかん。401エラーになってしまう…なんで?