Laravel11 × Nuxt3で、sanctumを使って認証機能を実装してみた（BE編）

## はじめに

みなさん、おはこんばんにちは。今回は、LaravelとNuxt.jsとSanctumを使って、認証機能を実装してみました。その備忘録等とか、アウトプットを兼ねて書いてみやす〜。

大前提ですが、まだまだ駆け出し的な身分なので、もしも変な記載とか間違ってるところがあった場合、誹謗中傷ではなく前向きなFBがもらえるとめっっっっっっちゃ嬉しいです〜。

## バージョン

### バックエンド
* PHP 8.3.10
* Laravel 11.20.0

### フロントエンド
* node 20.15.0
* npm 10.7.0
* Nuxt 3.12.4

こんな感じになっております。この記事では、バックエンドメインで書きますが、別記事でフロントエンドも書いていこうと思っております。

前提として、

* laravelの基本的なセットアップ（laravelのかっこいいトップページが表示されている）
* Nuxt3の基本的なセットアップ（Nuxt3のかっこいいトップページが表示されている）

ところまで完了していると仮定して、やっていきます。

また、Githubにもソースコードをあげてます〜。

https://github.com/takashiraki/nuxt_laravel_sanctum_be

## Laravel Sanctumのインストール

まずは、公式ドキュメントを参考に、Laravel Sanctumをインストールします。

```bash
php artisan install:api
```

コマンドで言うとこれっぽいですね。

これを実行すると、

* laravel/sanctumのアップデート or インストール
* `/api/user`ルートの作成
* `personal_accesstokens`テーブルの作成（これはyesの答えた場合のみですね）

がされてるっぽく

* `routes/api.php`
* `composer.json`
* `composer.lock`
* `config/sanctum.php`

等に対して変更がされてます。

```php:api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');

```

このようにユーザー情報を返すルートができてますね。実際にルート情報を確認してみると、

```bash
php artisan route:list
Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:9003 (through xdebug.client_host/xdebug.client_port).

  GET|HEAD       / ............................................................................................................................. generated::U5yHuUYjTAs5WbRm
  GET|HEAD       api/user ...................................................................................................................... generated::DemIuMPv3cZDFuo2
  GET|HEAD       sanctum/csrf-cookie ..................................................................... sanctum.csrf-cookie › Laravel\Sanctum › CsrfCookieController@show
  GET|HEAD       up ............................................................................................................................ generated::vAXMrfhkXs8J7sb1

                                                                                                                                                          Showing [4] routes
```

* ユーザー情報を取得するルート
* csrfトークンとかを含むレスポンスを返すルート

これらのルートが作成されています。

また、`composer.json`には、`laravel/sanctum`の追記がされています。

```json:composer.json
"laravel/sanctum": "^4.0",
```

こんな感じの追記がされています。多分無事にsanctumがインストールされていますね。

<details><summary>インストールコマンドの実行結果</summary>

```bash
php artisan install:api
./composer.json has been updated
Running composer update laravel/sanctum
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking laravel/sanctum (v4.0.2)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Downloading laravel/sanctum (v4.0.2)
  - Installing laravel/sanctum (v4.0.2): Extracting archive
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi

   INFO  Discovering packages.  

  laravel/sail .......................................................... DONE
  laravel/sanctum ....................................................... DONE
  laravel/tinker ........................................................ DONE
  nesbot/carbon ......................................................... DONE
  nunomaduro/collision .................................................. DONE
  nunomaduro/termwind ................................................... DONE

79 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
> @php artisan vendor:publish --tag=laravel-assets --ansi --force

   INFO  No publishable resources for tag [laravel-assets].  

No security vulnerability advisories found.

   INFO  Published API routes file.  

 One new database migration has been published. Would you like to run all pending database migrations? (yes/no) [yes]:
 > yes

   INFO  Running migrations.  

  2024_08_17_034630_create_personal_access_tokens_table ................................................................................ 9.13ms DONE


   INFO  API scaffolding installed. Please add the [Laravel\Sanctum\HasApiTokens] trait to your User model.  
```
</details>

次に、Sanctumミドルウェアを追加してみます。これも、公式ドキュメントによると、`bootstrap/app.php`にミドルウェアメソッドってのを呼ぶことで、簡単に実装できました。

```php:bootstrap/app.php
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // このメソッドを追加
        $middleware->statefulApi();
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

```

### Cookieの設定
続いて、cookieの設定をします。

公式ドキュメントには

> If you are having trouble authenticating with your application from an SPA that executes on a separate subdomain, you have likely misconfigured your CORS (Cross-Origin Resource Sharing) or session cookie settings.
> 
> 別のサブドメインで実行されるSPAからアプリケーションを認証する際に問題が発生する場合は、CORS (クロスオリジン リソース共有) またはセッション クッキーの設定が誤っている可能性があります。

と書いてまして、個人的には

* フロントエンド：`localhost:3000`
* バックエンド:`localhost:8080`

でやっているので、この設定はいらないかなと思っていたのですが、この設定をしないと、『CORS ポリシーによってブロックされました』と弾かれちゃったので、設定をしたところうまくいきました。

原因とかなぜかわかる人いたら教えて欲しいです、知識不足すんません...

と、言うわけで

```bash
php artisan config:publish cors
```

このコマンドを実行します。そうすると、`config/cors.php`が生成されます。

次に、`supports_credentials`の設定が`false`になっているので、`true`に変更します。

<details><summary>config/cors.phpの完成系</summary>

```php:config/cors.php
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Cross-Origin Resource Sharing (CORS) Configuration
    |--------------------------------------------------------------------------
    |
    | Here you may configure your settings for cross-origin resource sharing
    | or "CORS". This determines what cross-origin operations may execute
    | in web browsers. You are free to adjust these settings as needed.
    |
    | To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
    |
    */

    'paths' => ['api/*', 'login', 'logout', 'sanctum/csrf-cookie'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,

];
```
</details>

これで最低限の実装は多分完了しました。laravel10に比べてちょっと楽になったのかなって印象です。

## 認証機能を実装していく

今回は最低限

* サインイン
* アインアップ
* アインアウト
* ユーザー情報を返す

これらの機能のみ実装してみようと思います

### 実装

コントローラーとリクエストクラスを作っていきます。

```bash
# サインアップ系
php artisan make:controller Auth/SignUpController
php artisan make:request Auth/SignUpRequest

# サインイン系
php artisan make:controller Auth/SignInController
php artisan make:request Auth/SignInRequest

# サインアウト系
php artisan make:controller Auth/SignOutController
```

<details><summary>SignUpControllerの完成系</summary>

```php:app/Http/Controllers/Auth/SignUpController.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\SignUpRequest;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\DB;

class SignUpController extends Controller
{
    public function handle(
        SignUpRequest $http_request
    ) {
        DB::beginTransaction();
        try {
            User::create(
                [
                    'name' => $http_request->name,
                    'email' => $http_request->email,
                    'password' => $http_request->password,
                ]
            );
            DB::commit();
        } catch (Exception $exception) {
            DB::rollBack();
            return response()->json(
                [
                    'handle_error' => $exception->getMessage(),
                ],
                500
            );
        }

        return response()->json();
    }
}

```
</details>

<details><summary>SignUpRequestの完成系</summary>

```php:app/Http/Requests/Auth/SignUpRequest.php
<?php

declare(strict_types=1);

namespace App\Http\Requests\Auth;

use Illuminate\Foundation\Http\FormRequest;

class SignUpRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'between:1,256'],
            'email' => ['required', 'email:filter:dns', 'between:1,256', 'unique:App\Models\User,email'],
            'password' => ['required', 'string', 'confirmed'],
        ];
    }
}
```
</details>

<details><summary>SignInControllerの完成系</summary>

```php:app/Http/Controllers/Auth/SignInController.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\SignInRequest;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Support\Facades\Auth;

class SignInController extends Controller
{
    public function handle(
        SignInRequest $http_request
    ) {
        $credentials = $http_request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            $http_request->session()->regenerate();

            return response()->json(
                [
                    'message' => 'Authenticated',
                ]
            );
        }

        throw new AuthenticationException('Credentials error');
    }
}
```
</details>

<details><summary>SignInRequestの完成系</summary>

```php:app/Http/Requests/Auth/SignInRequest.php
<?php

declare(strict_types=1);

namespace App\Http\Requests\Auth;

use Illuminate\Foundation\Http\FormRequest;

class SignInRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'email' => ['required', 'email:filter:dns', 'between:1,256'],
            'password' => ['required', 'string'],
        ];
    }
}
```
</details>

<details><summary>SignOutControllerの完成系</summary>

```php:app/Http/Controllers/Auth/SignUpController.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class SignOutController extends Controller
{
    public function handle(Request $http_request)
    {
        Auth::guard('web')->logout();

        $http_request->session()->invalidate();
        $http_request->session()->regenerateToken();

        return response()->json();
    }
}
```
</details>

<details><summary>routes/web.phpの完成系</summary>

```php:routes/web.php
<?php

declare(strict_types=1);

use App\Http\Controllers\Auth\SignInController;
use App\Http\Controllers\Auth\SignOutController;
use Illuminate\Support\Facades\Route;

Route::post('/login', [SignInController::class, 'handle']);
Route::post('/logout', [SignOutController::class, 'handle']);
```
</details>

<details><summary>routes/api.phpの完成系</summary>

```php:routes/api.php
<?php

declare(strict_types=1);

use App\Http\Controllers\Auth\SignUpController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('/signup', [SignUpController::class, 'handle']);

Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');
```
</details>

## 参考文献

https://laravel.com/docs/11.x/sanctum

https://qiita.com/ucan-lab/items/3e7045e49658763a9566