【Laravel】Sanctumで認証API開発方法!ログイン/ログアウトとトークン管理

この記事からわかること
- Laravelで認証APIを開発する流れ
- Sanctumの導入と使い方
- auth.php、sanctum.phpとは?
- 未認証時のハンドリング方法
index
[open]
\ アプリをリリースしました /
環境
- Laravel:8.83.29
- PHP:8.2.0
- macOS:Sonoma 14.6.1
Laravel Sanctumとは?
「Laravel Sanctum」とはLaravelにおける認証システムの1つでトークンベースでの認証APIを開発することが可能になっています。主にSPA(シングルページアプリケーション)やモバイルアプリケーションと連携して認証機能を実装するのに活用することが可能です。
APIトークン
SanctumはAPIトークンを発行してユーザー認証を行うシンプルな設計です。ユーザーのログイン時などにトークンを発行しそのトークンを使用して認証を行い不正なアクセスでないかを検知します。有効期限は年単位で設定できるようですがユーザーのタイミングでトークンをを無効にすることも可能になっています。
APIトークンをデータベーステーブルに保存し、有効なAPIトークンを含む必要があるAuthorization
ヘッダを介して受信し、HTTPリクエストを認証するといった仕組みになっています。
導入手順
最新のLaravelにはデフォルトでSanctumが導入されています。私の動作環境ではLaravel:8.83.29
で導入済みであることを確認できました。確認方法はcomposer.json
ファイル内にlaravel/sanctum
を含んでるかどうかで確認できます。
もし未導入の場合は以下のコマンドを実行します。
$ composer require laravel/sanctum
次に以下のコマンドを実行してSanctum設定ファイルとマイグレーションファイルをリソース公開しておきます。
$ php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
ここでAPIトークンを格納するためのデータベーステーブルを生成するためにmigrate
を実行します。
$ php artisan migrate
最後にapp/Http/Kernel.php
ファイル内のapi
ミドルウェアグループに追加して導入は完了です。
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
APIの開発実装手順
認証APIを開発する上で必要になるクラスなどを先にまとめておきます。
- Userモデル
- 認証ハンドリングを行うController
- ルーティング(api.php)
Userモデル
Laravel Sanctumがデフォルトで入っているおかげかUser
モデルもデフォルトで用意されています。HasApiTokens
トレイトが記述してあればOKです。
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
];
}
認証ハンドリングを行うController
続いて認証ハンドリングを行うためのController
を作成します。
$ php artisan make:controller AuthController
作成できたら「新規登録」、「ログイン」、「ログアウト」、「User情報取得」の4つのメソッドを実装していきます。User
モデルを使用してそのままDBへのCRUD処理が実装できるので楽に実装可能です。トークンを取得したい場合はcreateToken('auth_token')->plainTextToken
でAPIトークンを取得することが可能です。
class AuthController extends Controller
{
// 新規登録
public function register(Request $request)
{
$validatedData = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $validatedData['name'],
'email' => $validatedData['email'],
'password' => Hash::make($validatedData['password']),
]);
return response()->json(['message' => 'User registered successfully'], 201);
}
// ログイン
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
]);
}
// ログアウト
public function logout(Request $request)
{
$request->user()->tokens()->delete();
return response()->json(['message' => 'Logged out successfully']);
}
// 認証ユーザー情報
public function user(Request $request)
{
return response()->json($request->user());
}
}
またトークンを明示的に破棄するには$request->user()->tokens()->delete()
を使用します。
ルーティング(api.php)
最後にルーディングです。APIなのでapi.php
に記述します。Sanctumを使用して認証を施したいルートに対してRoute::middleware('auth:sanctum')
を付与します。以下の場合ユーザー情報取得とログアウト処理にのみ認証が必須になります。
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->get('user', [AuthController::class, 'user']);
Route::middleware('auth:sanctum')->post('logout', [AuthController::class, 'logout']);
APIの疎通テスト
これでAPIの開発が完了しました。実際に正常に動作するか確認していきます。今回は「Postman」を使用して疎通テストしていきます。
新規登録
データベースが空の状態なので最初に新規登録を行います。新規登録はPOST
メソッドでhttp://localhost/oiwai/api/register
にアクセスします。その際にbody
にJSON形式でユーザー登録情報を渡します。
※oiwai
は私の環境構造上入っているだけなので抜いてください。
{
"name": "テスト太郎",
"email": "test@example.com",
"password": "password123",
"password_confirmation": "password123"
}
Send
ボタンで実行するとデータベースにユーザーが登録されます。

ログイン
続いてログインしたいのでPOST
メソッドでhttp://localhost/oiwai/api/login
にアクセスします。こちらもbody
にJSON形式でユーザーログイン情報を渡します。
{
"email": "test@example.com",
"password": "password123"
}
するとレスポンスでAPIトークンを返すように設定していたのでトークンを取得できます。
{
"access_token": "4|Jr8DaKo6VTSg2WhLB63wyWbRGWsf04MIPb5UMvIl",
"token_type": "Bearer"
}

認証してアクセスする
最後に認証が必要なルートへアクセスする方法です。GET
メソッドでhttp://localhost/oiwai/api/user
にアクセスします。先ほど取得したトークンはAuthorization
ヘッダーに渡します。Postmanの場合は以下のようにBearer Token
を指定して、該当のトークンを記述します。
しかしなぜかサーバーエラー:500
になってしまいました。500になっている原因は未認証でリダイレクトが走り、表示されているエラー:Route [login] not defined.
はlogin
Viewが未定義なので正しいのですが、トークンを正しく指定しても認証に失敗してしまいました。解決方法は模索中です・・・

※追記:2025/1/25 認証ルートの疎通ですがPostmanだと成功せず、php artisan serve
でサーバーを立ち上げてテストした場合は正常に動作しました。環境上oiwai
が入っていたりするのが良くなかったのかもしれません。
// Laravelサーバー立ち上げ
$ php artisan serve
// 新規登録
$ curl -X POST http://127.0.0.1:8000/api/register \
-H "Content-Type: application/json" \
-d '{
"name": "testuser",
"email": "testuser@example.com",
"password": "password123",
"password_confirmation": "password123"
}'
// Response
{"message":"User registered successfully"}
// ログイン
$ curl -X POST http://127.0.0.1:8000/api/login \
-H "Content-Type: application/json" \
-d '{
"email": "testuser@example.com",
"password": "password123"
}'
// Response
{"access_token":"5|TNbh9DywCZRfQBCAiI6NjqCxFld8D77HPeAM68ot","token_type":"Bearer"}
// 認証ルート(ユーザー情報取得)
$ curl -X GET http://127.0.0.1:8000/api/user \
-H "Authorization: Bearer 5|TNbh9DywCZRfQBCAiI6NjqCxFld8D77HPeAM68ot" \
-H "Content-Type: application/json"
// Response
{"id":2,"name":"testuser","email":"testuser@example.com","email_verified_at":null,"created_at":"2025-01-24T14:52:30.000000Z","updated_at":"2025-01-24T14:52:30.000000Z"}
※追記:2025/2/2 ちなみにhttp://localhost/
で認証失敗していた原因はURLのpublicを消すために.htaccessを設置したことでした。
追加した際に以下を入れておかないとAuthorization ヘッダーをリダイレクト先に回せず抜け落ちてしまっていたみたいですね・・・
# Authorization ヘッダーを環境変数に保持
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [E=HTTP_AUTHORIZATION:%1]
Authorizationヘッダーに渡す値
ヘッダーにはAuthorization
をKey
として値にトークンを渡します。トークンはBearer トークン
形式で渡さないと正常に認証できないので注意してください。
またレスポンスで取得したトークンは接頭辞に5|
が付与されています。これはデータベースのトークンIDで5|
を付与しなくても認証は通ります。
5|TNbh9DywCZRfQBCAiI6NjqCxFld8D77HPeAM68ot
またデータベースに格納されているトークンはハッシュ化されたものになるので認証で使用するのは$user->createToken('auth_token')->plainTextToken
で取得した生のトークンを使用してください。
未認証時のハンドリング
認証に失敗した際にどのようなハンドリングが行われているのか調べてみました。まずlogin
ViewへリダイレクトしていたのはApp/Http.Middleware/Authenticate.php
でした。
class Authenticate extends Middleware
{
// ユーザーが認証されていない場合にリダイレクトされるパスを取得します。
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
}
そしてredirectTo
自体はvendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php
のunauthenticated
メソッドから呼び出されているぽいですね。
protected function unauthenticated($request, AuthenticationException $exception)
{
return $this->shouldReturnJson($request, $exception)
? response()->json(['message' => $exception->getMessage()], 401)
: redirect()->guest($exception->redirectTo() ?? route('login'));
}
vendor
の中ではなくapp/Exception/Handler.php
にも同名のものが存在しています。ここでunauthenticated
をオーバーライドすれば任意のハンドリングが行えるかもです。
// 認証失敗時のレスポンスをカスタマイズ
protected function unauthenticated($request, AuthenticationException $exception)
{
return response()->json([
'message' => '認証に失敗しました。トークンが無効または期限切れです。',
], 401);
}
Sanctumの設定ファイル
Sanctumの設定はconfig/sanctum.php
で管理されています。
Cookieベースの認証をどのドメインで許可するか
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
トークンの有効期限
null
は無制限。有効期限を設定したい場合は、分単位で数値を指定。(例: 60 で1時間)。
'expiration' => null,
Middleware Sanctumの認証で使用するミドルウェアを指定
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
APIトークン認証のガード
'guard' => ['web'],
Laravel自体の認証設定ファイル
Laravel自体の認証設定config/auth.php
で管理されています。
デフォルトで使用する認証ガード
'defaults' => [
'guard' => 'sanctum',
'passwords' => 'users',
],
認証の仕組みを設定
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'sanctum' => [
'driver' => 'sanctum',
'provider' => 'users', // 対象となるユーザー情報
],
],
ユーザー情報を取得するデータベース設定
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
],
パスワードリセット
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。