Skip to main content
Laravel, shipping fast.
Chapter 4 · Authentication

Expiry, Rotation, Revocation

Julian Beaujardin

Sanctum checks the token on every request. That costs a query for the token, a query for its consumer, and a write to last_used_at. It also has a consequence people underrate: revocation is immediate.

$consumer->tokens()->where('name', 'production')->delete();

The next request with that token gets a 401. There is no cache to wait out.

I used to cache token lookups to save those queries, and I had to write paragraphs explaining how long a revoked token stays valid. That is the trade. Don’t make it until you have measured that authentication is your bottleneck, and Chapter 9 is about how to measure.

Rotation is issuing a new token before the old one expires, and deleting the old one once the consumer has switched. Because a consumer can hold several tokens, the two overlap and nothing goes down.

Expired tokens stop working on their own, but their rows stay. Schedule Sanctum’s command to clear them:

// routes/console.php
Schedule::command('sanctum:prune-expired')->daily();

And look at last_used_at now and then. A token that hasn’t been used in three months belongs to a service that no longer exists.

Testing It

Sanctum’s test helper authenticates a model with a set of abilities, without creating a real token:

// tests/Feature/AuthTest.php
it('rejects a request without a token', function () {
    $this->getJson('/api/licenses')->assertUnauthorized();
});

it('forbids writing with a read-only token', function () {
    Sanctum::actingAs(
        Consumer::factory()->create(),
        ['licenses:read'],
    );

    $this->postJson('/api/licenses', [
        'name' => 'Acme',
        'domain' => 'acme.test',
    ])->assertForbidden();
});

Write the second kind for every ability you define. Until a test like it exists, nobody has checked that the ability does anything.

Give actingAs the exact abilities the test is about, never ['*']. A wildcard passes every check, so a test that uses it proves nothing about authorization.

One more test belongs here, because it guards the claim this chapter rests on. It checks that a request is made to the provider with the caller’s credentials and not somebody else’s:

it('calls the provider as the consumer', function () {
    Http::fake(['*' => Http::response(['data' => []])]);

    $consumer = Consumer::factory()
        ->forPartner('partner-token')
        ->create();

    Sanctum::actingAs($consumer, ['licenses:read']);

    $this->getJson('/api/licenses')->assertOk();

    Http::assertSent(fn ($request) => $request->hasHeader(
        'Authorization',
        'Bearer partner-token',
    ));
});

forPartner() is a factory state that puts a provider token in settings. Chapter 12 shows it. From here on, tests in this book that call a protected route authenticate first, usually in a beforeEach. That includes Chapter 2’s test, which now needs a caller before it can reach validation.

When Not to Use This

  • A browser application of your own. Use Sanctum’s cookie-based SPA authentication, not tokens in local storage.
  • Third-party developers who need to act for your users. That is OAuth, and Laravel Passport implements it.
  • A public, anonymous API. Skip authentication and lean on the rate limiting in Chapter 6.

Chapter 4 Summary

  • Authenticate services with Sanctum API tokens on a model of their own. A tokenable model doesn’t have to be a person.
  • Tokens are stored hashed. Always pass abilities and an expiry, and never issue *.
  • auth:sanctum on the route group. $request->user() is the consumer.
  • Declare abilities on the controller, closed by default. A new controller is open until you close it.
  • Per-tenant credentials fail closed. No shared fallback.
  • Anything that depends on who is asking is scoped, not a singleton.
  • Don’t cache authentication until you’ve measured it. Immediate revocation is worth the queries.

The audio could not be loaded. Try again in a moment.