Token Authentication

Sanctum authenticates an API two ways: hashed bearer tokens carrying abilities, or your own front end's session cookie.

Syntax$user->createToken(string $name, array $abilities = ['*'], ?DateTimeInterface $expiresAt = null): NewAccessToken

Sanctum is Laravel's API authentication package, and php artisan install:api installs it. It answers two different questions with one guard, auth:sanctum.

Token mode

Add the Laravel\Sanctum\HasApiTokens trait to your User model; install:api prints a reminder but does not edit the file. $user->createToken('Phone', ['posts:write']) then returns a Laravel\Sanctum\NewAccessToken. Read its plainTextToken property once and show it to the user: the personal_access_tokens table stores a SHA-256 hash of the token, so it cannot be recovered later, only replaced. The client sends it as an Authorization: Bearer header.

Abilities

Abilities are the strings you passed to createToken, defaulting to ['*']. Test one with $user->tokenCan('posts:write') or tokenCant(). Sanctum also ships two middleware, CheckAbilities and CheckForAnyAbility, which you register as the abilities and ability aliases in bootstrap/app.php before naming them on a route.

Cookie mode

For your own single-page front end, call $middleware->statefulApi() in bootstrap/app.php. That prepends EnsureFrontendRequestsAreStateful to the api group. A request from a domain listed in config/sanctum.php's stateful array is then authenticated by the ordinary session cookie, after the front end has called /sanctum/csrf-cookie. No token is involved. Sanctum checks for that cookie first and only looks at the Authorization header when it is absent.

Revoking

A token is a database row, so revoking is a delete.

Example

Example · php
<?php
// app/Http/Controllers/Api/TokenController.php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class TokenController extends Controller
{
    /**
     * Exchange credentials for a token. POST /api/tokens
     */
    public function store(Request $request): JsonResponse
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required', 'string'],
            'device_name' => ['required', 'string', 'max:100'],
        ]);

        $user = User::query()->where('email', $credentials['email'])->first();

        // One failure message for both cases: a different message for a
        // missing email tells an attacker which addresses are registered.
        if (! $user || ! Hash::check($credentials['password'], $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }

        $token = $user->createToken(
            $credentials['device_name'],
            ['posts:read', 'posts:write'],
        );

        // plainTextToken exists only here. The row holds a SHA-256 hash.
        return response()->json([
            'token' => $token->plainTextToken,
        ], 201);
    }

    /**
     * Revoke the token that made this request. DELETE /api/tokens/current
     */
    public function destroy(Request $request): Response
    {
        $request->user()->currentAccessToken()->delete();

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

When to use it

  • Let a user generate a personal access token in account settings for scripting against your API.
  • Authenticate a mobile app that exchanges email and password for a long-lived token.
  • Authenticate your own Vue or React front end with the session cookie instead of a token.
  • Issue a token that may read posts but not delete them.

More examples

The trait, the aliases and the routes

Three files have to agree before a token works: the trait gives the model its tokens relationship, the aliases make the ability middleware nameable, and the guard on the route resolves the user. A missing trait produces a call to an undefined method createToken, not a 401.

Example · php
<?php
// app/Models/User.php — install:api does not add this trait for you.

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

// bootstrap/app.php — register Sanctum's ability middleware under short names.

use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'abilities' => CheckAbilities::class,   // token must have ALL of them
        'ability' => CheckForAnyAbility::class, // token must have ONE of them
    ]);

    // Cookie authentication for your own SPA on a stateful domain.
    $middleware->statefulApi();
})

// routes/api.php

Route::middleware(['auth:sanctum', 'abilities:posts:write'])->group(function (): void {
    Route::post('/posts', [PostController::class, 'store']);
    Route::delete('/posts/{post}', [PostController::class, 'destroy']);
});

Route::get('/posts', [PostController::class, 'index'])
    ->middleware(['auth:sanctum', 'ability:posts:read,posts:write']);

An account settings screen: list and revoke

What a personal access token screen needs. last_used_at is maintained by Sanctum on each authenticated request, which makes it the fastest way to spot a token no client has used for months.

Example · php
<?php

// Tokens are models, so the toolkit applies. $user->tokens is a relationship
// added by HasApiTokens.
$tokens = $request->user()->tokens
    ->map(fn ($token): array => [
        'id' => $token->id,
        'name' => $token->name,
        'abilities' => $token->abilities,
        'last_used_at' => $token->last_used_at?->diffForHumans(),
        'is_current' => $token->id === $request->user()->currentAccessToken()->id,
    ]);

// Revoke one, by id, scoped to the owner so a user cannot delete
// someone else's token by guessing a number.
$request->user()->tokens()->where('id', $tokenId)->delete();

// Revoke everything, for a "sign out everywhere" button.
$request->user()->tokens()->delete();

Expiring tokens

Use a short expiry for anything machine-issued, such as a CI key, and leave long-lived tokens to humans who can see and revoke them. Pruning is housekeeping only: an expired token stops authenticating the moment it expires, whether or not the row is still there.

Example · php
<?php
// config/sanctum.php — minutes until a token is considered expired.
// By default this is null and tokens never expire.

'expiration' => 525600, // one year

// Per token, as the third argument. This overrides the config value.
$token = $user->createToken('CI deploy key', ['deploy'], now()->plus(weeks: 1));

// Expired rows stay in the table until you remove them. Sanctum ships
// a command for that:
//
//   php artisan sanctum:prune-expired --hours=24

Discussion

  • Be the first to comment on this lesson.