Laravel Sanctum (SPA認証) がしてくれること

# 概要
Laravel SanctumはSPAやスマホアプリなどで利用するバックエンドWeb API用の認証の仕組みを提供していくれるが、いまいち何をしてくれるのか分かりづらかったので、ソースコードを紐解いて具体的に何をしているのか調べてみた。

SanctumにはAPIトークン認証とSPA認証の２つの方式がある。

<dl>
  <dt>APIトークン認証</dt>
  <dd>認証後、トークンを発行しアプリに渡し、アプリはそのトークンを使用してプライベートなWeb APIにアクセスする。主にスマホアプリなどで利用する。</dd>
  <dt>SPA認証</dt>
  <dd>認証後、アプリにトークンなどを渡すこと無く、そのWeb APIのドメインのCookieに認証済みのセッションを記録する。クライアントアプリに秘匿情報が渡らないので、よりセキュアな仕組みとなる。</dd>
</dl>

今回はSPA認証について説明する。

#### Sanctumがやってくれること３つ
* Web APIのエンドポイントをステートフルにしてくれる。
* Guardを実装してくれる。
* CSRFトークンを提供するAPIを実装してくれる。

#### Sanctumがやらないこと
* ログインページなどのルーティングやフロントページの実装  
  必要な場合は、スキャフォールディングや、[Fortify](https://laravel.com/docs/8.x/fortify)で実装したりする。
* ユーザプロバイダの実装  
  特別な要件がない限り、Laravelに既存の`database`と`eloquent`のいずれかを利用するのが一般的。

# Sanctumがやってくれること詳細
まず、基礎を知るために下記2つを読んでおくことをお薦めする。

* [Laravel公式ドキュメント - Sacntum SPA認証](https://laravel.com/docs/8.x/sanctum#spa-authentication) は大雑把な内容だが、Sactumをどう使うべきかを示している。
* [Laravel Sanctum Explained : SPA Authentication](https://dev.to/nicolus/laravel-sanctum-explained-spa-authentication-45g1)はSanctumがしていることをわかりやすく解説している。

## 1. Web APIのエンドポイントをステートフルにしてくれる
Laravelのデフォルトauthでは、Webページと異なりWeb APIのエンドポイントはステートフルではない（セッションが作成されない）ので認証でアクセスを制限できない。ステートフルではないと言うことは、サーバ側で（認証を含め）状態を維持しないと言うことなので、認証状態をアクセス制限に使用することができない。

Sanctumは、Web APIにも「必要に応じてセッションを作成する」機能を提供する。

`app/Http/Kernel.php`を見ると、Laravelのデフォルトでは`route/web.php`に記述したエンドポイントには`StartSession`などのミドルウェアが適用されるのでステートフルになる。`app/Http/Kernel.php`のapiミドルウェアグループに`SanctumのEnsureFrontendRequestsAreStateful`クラスを追加することで、これをやってもらう。

Sanctumの`EnsureFrontendRequestsAreStateful`の実装を見ると、やっていることは`web`ミドルウェアグループとほとんど同じミドルウェアを適用することだが、`web`ミドルウェアグループとの違いは、`SANCTUM_STATEFUL_DOMAINS`に列挙したドメインからのアクセスに限定するというところになる。（Web APIへのリクエストの`referer`または`origin`ヘッダーに設定されたドメインが、`SANCTUM_STATEFUL_DOMAINS`に含まれているかどうかをチェックしている。）それ以外のドメイン（`localhost`などはデフォルトで追加される）からのアクセスではセッションが使用されずステートフルにならない。これにより、意図しない外部サイトがログイン状態を使用して我々のWeb APIにアクセスしてしまうのを防止する。

ちなみに、Sanctumの`EnsureFrontendRequestsAreStateful`ではエンドポイントをステートフルにしているだけで、アクセス制限をかけているわけではない。アクセス制限は、更に`auth`ミドルウェアを通すことで実現する。（この`auth`ミドルウェアが機能するためにはステートフルである必要がある。）

## 2. Guardを実装してくれる
`sanctum`という名称のGuardを実装してくれる。Guardに関しては特に設定は不要で、`Route::middleware('auth:sanctum')`のように`auth`ミドルウェアを使用する際にこのガードを指定できる。

Guardの実装は`config/auth.php`などで定義する必要はなく、自動的に作成＆挿入してくれる。

`sanctum`ガードがすることは、大まかに下記２点

* `config/sanctum.php`で`guard`で指定するガードドライバを使用する
* APIトークンを使用している場合に、トークンをチェックする（SPA認証なので、無視）

`config/sanctum.php`での`guard`が指定されていなければ、`web`ガードが使用される[^1]。そして、特別な理由がない限り指定する必要はないので、結局`config/auth.php`の`guards`に定義されている`web`ガードが使用される。

[^1]: Sanctumのガードは`SessionGuard`などと同じ`\Illuminate\Contracts\Auth\Guard`インタフェースを実装した`\Illuminate\Auth\RequestGuard`で、`\Laravel\Sanctum\SanctumServiceProvider::createGuard`でインスタンス化されている。このガードインスタンスは`callback`が呼ばれると、実際には`\Laravel\Sanctum\Guard::__invoke`を呼び出す。この`__invoke`は`Guard::user()`から呼び出され、認証済みのユーザを返すとこが期待されている。`__invoke`の中では、更に`config('sanctum.guard', 'web')`で取得したガードを使用して`User`モデルを取得している。つまり、`sanctum.guard.provider`にuser providerを指定しない限り、`web`ガードを使用して`User`モデルを使用している。そのため、特に指定しなければ`web`に指定したUser providerが使用されることになる。

このためSanctumのデフォルトのガード設定だと`auth:sanctum`と`auth:web`に違いが無いように見える（実際にはあるかもしれない）が、保守性を考慮しても`auth:sanctum`を使用しない理由はない。

また、[公式ドキュメント](https://laravel.com/docs/8.x/sanctum#protecting-routes)によれば、`routes/web.php`でのアクセス制限も`auth:sanctum`を使用する必要があるようだ。

## 3. CSRFトークンを提供するAPIを実装してくれる
`/sanctum/csrf-cookie`というエンドポイントが自動的に作成される。`SanctumServiceProvider`が動的に挿入している。

このエンドポイントにアクセスすると、レスポンスボディではなく`XSRF-TOKEN`というCookieに乗ってくるので、フロントエンドではこれを抽出して使用する。これはAxiosなどの流儀に従っているため、Axiosは自動的に抽出して`X-XSRF-TOKEN`というヘッダーに設定してくれる。

#### CSRFトークンを使用するわけ
Web APIエンドポイントはSameOriginPolicyによりCORS設定で許可されていない外部サイトがJavaScriptなどから利用できないように保護されている。しかし、リクエストができないわけではなく、レスポンスが使用できないだけなので、`UPDATE`や`DELETE`などのHTTPメソッドがリクエストできてしまう。そのリクエストのドメインはリクエスト先のものであるためブラウザが認証済であれば、そのリクエストは実行されてしまう。これを防止するために、そのリクエストが許可されたサイトからのものかを確認するためにCSRFトークンが利用される。

CSRFトークンも同様に誰でもリクエストできてしまうが、CORS設定で許可されていないサイトではレスポンスを受け取れないため、このCSRFトークンを取得できない。よって、CSRFトークン付きで送られてくるリクエストはCORSで許可されたサイトからのリクエストであることが保証される。

CSRFに関しては下記記事がとても良く説明してくれている。感謝！

https://qiita.com/okamoai/items/044c03680766f0609d41

以上！

LaravelやSanctumが色々とやってくれるとは言え、システムによって認証の要件もまちまち（例えばSSOしたい！とか）なので、具体的にSanctumが何をどうしてくれるのか理解していないと、実用的なシステムを構築するのは難しい。