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

Dynamic Configuration via Token Settings

Julian Beaujardin

The LicenseServiceProvider is where the settings property comes to life. When a request arrives with a token, the provider examines the token's settings and configures the application for that specific integration.

// app/Providers/LicenseServiceProvider.php
final class LicenseServiceProvider extends ServiceProvider
{
    use CachesBearerTokens;

    public function register(): void
    {
        $this->app->singleton(LicenseManager::class);
    }

    public function boot(): void
    {
        $token = request()->bearerToken();
        if (! $token) {
            return;
        }

        // Get cached Bearer instance (same cache hit as middleware)
        $bearer = $this->rememberBearerToken($token);
        if (! $bearer instanceof Bearer) {
            return;
        }

        // Extract configuration from token settings
        $settings = $bearer->settings;
        if (! is_array($settings)) {
            return;
        }

        $licenseSettings = $settings['license'] ?? null;
        if (! is_array($licenseSettings)) {
            return;
        }

        // Set which driver to use (Statamic, Filament, etc.)
        $driver = $licenseSettings['driver'] ?? null;
        if (! is_string($driver)) {
            return;
        }

        Config::set('services.license.default', $driver);

        // Set driver credentials (e.g., Statamic API token)
        $driverToken = $licenseSettings['token'] ?? null;
        if (is_string($driverToken)) {
            Config::set("services.license.drivers.{$driver}.token", $driverToken);
        }
    }
}

Real example. You create two Bearer tokens:

Token 1: Used by Partner A

{
    "license": {
        "driver": "statamic",
        "token": "partner_a_statamic_token_secret"
    }
}

Token 2: Used by Partner B

{
    "license": {
        "driver": "statamic",
        "token": "partner_b_statamic_token_secret"
    }
}

Both tokens authenticate to the same API. But Partner A's requests use Partner A's Statamic token. Partner B's requests use Partner B's token. The same LicenseController, the same LicenseService, the same everything. Just different configuration per token.

This is multi-tenancy without microservices, without routing complexity, without tenant tables in your database. It's all in the token.

Middlewares

Middleware is the interceptor layer between a request arriving at your API and it reaching your controller. Think of it as a series of doors the request must pass through. Each door can examine the request, modify it, pass it through unchanged, or slam it shut and send the request back without ever reaching your controller.

Every middleware does one job:

  • Validate a Bearer token exists
  • Check if the request exceeds a rate limit
  • Add security headers to the response
  • Log what happened
  • Detect the client's preferred language
  • Compress the response body

Each middleware receives a request, decides what to do with it, then passes it to the next middleware in the chain. The response comes back through the same chain in reverse order, so each middleware also gets a chance to transform the response before it leaves your API. That is why order matters. A request rejected by authentication middleware never reaches rate limiting. A response compressed last gets the proper content-length headers.

Think of it like an airport security line. Each checkpoint does one thing:

  • Check-in verifies your ticket.
  • Security scans your bags.
  • Passport control verifies your identity.
  • The gate confirms you're on the right flight.

If any checkpoint rejects you, you do not board. You do not move to the next checkpoint. Middleware works the same way.

In this API, the protected routes for LicenseController are grouped with route middleware. The order is deliberate and matches how the request should be vetted:

// routes/api.php
Route::middleware([
    EnsureTokenIsValidMiddleware::class,
    'throttle:api-token',
    AddRateLimitHeadersMiddleware::class,
    LogApiRequestsMiddleware::class,
])->controller(LicenseController::class)->group(function () {
    // routes
});

The /health route stays public, so it does not use these route middleware. It only gets the global API middleware we add later.

EnsureTokenIsValidMiddleware: Token Validation

Every API request must prove its identity. You cannot just accept any token that shows up in the Authorization header. You need to verify it, check it has not expired, and optionally validate that the request origin is allowed. That is what EnsureTokenIsValidMiddleware does.

This middleware extends VerifyBearerToken from Chandler's package, adding company-specific validation logic on top of the base Bearer token verification:

// app/Http/Middleware/EnsureTokenIsValidMiddleware.php
class EnsureTokenIsValidMiddleware extends VerifyBearerToken
{
    use CachesBearerTokens;

    public function handle(Request $request, Closure $next): Response
    {
        $token = $request->bearerToken();

        if (! is_string($token)) {
            return parent::handle($request, $next);
        }

        $foundToken = $this->rememberBearerToken($token);

        if (! $foundToken ||
            $foundToken->expired ||
            ! $this->isTokenValidForDomain($request, $foundToken)) {
            return parent::handle($request, $next);
        }

        return $next($request);
    }
}

When a request arrives, this middleware performs three critical checks in sequence:

The middleware first calls $request->bearerToken() to extract the token from the Authorization: Bearer ... header. If there is no token at all, it immediately delegates to the parent handler, which will reject the request with a 401. This is clean, because if there is no token, there is nothing custom to validate.

Next, it calls $this->rememberBearerToken($token). This method from the CachesBearerTokens trait checks the cache first, and if missed, queries the database with Bearer::where('token', $token)->first() and caches the result. If the token does not exist in the database, the middleware rejects with error code 401. If it does exist, you now have a Bearer instance with all its properties available: expiration time, domain restrictions, settings, and everything else.

Once you have the Bearer instance, you check $foundToken->expired. This is a computed property on the Bearer model that compares the token's expires_at timestamp against the current time. If it is expired, rejecting with 401 is the right move. Expired tokens are useless.

Finally, if the token has domain restrictions configured, you validate that the request is coming from an allowed domain. This is where isTokenValidForDomain() comes in. It allows you to bind tokens to specific domains, adding an extra security layer.

Token Extraction and Type Check
Token Lookup and Existence
Expiration Check
Domain Validation

Domain validation is optional but powerful. Tokens can specify which domains are allowed to use them. This is crucial in multi-account or partner scenarios where you want to ensure that a token issued to a specific client can only be used from their domain.

The isTokenValidForDomain() method handles several edge cases:

Domains Configuration Check: First, it checks if domains are even configured on this token. If $token->domains is empty, or if the global bearer.verify_domains configuration is disabled, domain validation is skipped. This means you can use the same middleware everywhere, and it gracefully skips domain checking for tokens that do not have domain restrictions. Flexibility without complexity.

JSON Parsing: Domains might be stored as a JSON string in the database (if they came from an external API or were set that way). The middleware tries to decode them: json_decode($domains, true). If the decode fails or does not return an array, the token is rejected. This guards against malformed data.

Exact Domain Matching: Finally, it compares the request's origin ($request->getSchemeAndHttpHost()) against the allowed domains. This uses in_array(..., true) with strict comparison. The getSchemeAndHttpHost() returns the full origin including protocol, like https://example.com. By matching this exactly, token theft is harder, because an attacker who gets a token bound to https://example.com cannot just request from http://example.com or https://attacker.com.

If any of these checks fail, whether missing token, invalid token, expired token, or wrong domain, the middleware calls parent::handle($request, $next). This is the crucial part: you are delegating to Chandler's VerifyBearerToken middleware, which properly handles the rejection. It returns a 401 Unauthorized response with appropriate headers. Your custom logic does not have to reimplement error handling, it just defers to the framework.

When validation passes, the middleware calls return $next($request). The request reaches your controller with the authenticated bearer available. You can access it like this:

$bearer = Auth::user();  // The authenticated bearer

The bearer becomes available as the authenticated "user" for the request, even though it is not a user. Internally, Laravel treats authenticated bearers as guard-backed users, so all the familiar authentication patterns work. Imagine two clients, Client A and Client B, both using your API:

{
    "token": "abc123xyz",
    "expires_at": "2026-12-31",
    "domains": ["https://client-a.com"],
    "settings": {
        "license": {
            "driver": "statamic",
            "token": "statamic_token_for_a"
        }
    }
}
{
    "token": "def456uvw",
    "expires_at": "2026-12-31",
    "domains": ["https://client-b.com"],
    "settings": {
        "license": {
            "driver": "filament",
            "token": "filament_token_for_b"
        }
    }
}

When Client A sends a request from https://client-a.com with their token, the middleware:

  1. Finds their Bearer token in the database ✓
  2. Checks it is not expired ✓
  3. Verifies the domain matches ✓
  4. Passes the request to the controller

Client A's request later reaches LicenseServiceProvider::boot(), which checks the bearer's settings and configures the application to use Statamic with Client A's credentials. Perfect.

If Client B tried to use Client A's token from their domain, the domain validation would fail and the request would be rejected. If Client B used their own token but from the wrong domain, same result. This prevents token misuse.

Here is a performance trick: cache your tokens. Looking up a token in the database on every request means a database query on every single API call. That adds up fast.

Instead, cache the token for a short time. If it is revoked or expires in the meantime, the cache will miss and you will do a fresh lookup. This is a small optimization but it matters at scale.

Token Revocation Strategy

Token revocation is a common concern. There are two approaches, each with tradeoffs:

Approach 1: Immediate Revocation (No Caching)

  • Query the database on every request
  • Revoked tokens rejected instantly
  • Downside: 2-3x database load, slower response times
  • Use when: Security is critical (e.g., after detecting abuse)

Approach 2: Cache-Based Revocation (This Project)

  • Cache tokens as we do here
  • Revoked tokens remain valid until cache expires (60-3600s depending on token expiration)
  • Downside: Revocation takes up to cache TTL to take effect
  • Upside: far fewer database queries, scales to thousands of requests/second
  • Use when: You can tolerate temporary validity of revoked tokens

For this API, we use caching because the cache TTL is short (60 seconds minimum). If you revoke a token right now, attackers can use it for at most 60 more seconds, usually fast enough for most emergency situations while keeping the API performant.

If you need instant revocation, you can maintain a separate "revoked tokens" cache alongside the bearer cache. Check the revoked list before accepting a cached bearer. This gives you both performance and immediate revocation.

Token Caching: CachesBearerTokens Trait

Both EnsureTokenIsValidMiddleware and LicenseServiceProvider call rememberBearerToken() to look up Bearer tokens. This is intentional, Bearer tokens are looked up frequently, and database queries add up fast. By caching the token, you avoid a database hit on every single API request.

The caching logic is extracted into a reusable CachesBearerTokens trait:

The trait was shown in full earlier in this chapter; the two methods that matter here are rememberBearerToken(), which both callers use, and calculateBearerCacheTTL(), which decides how long the entry lives.

This trait provides two methods: rememberBearerToken($token): This is the private method that both the middleware and service provider call. It wraps the caching logic in a clean, reusable interface. It uses Cache::get() to check if the token is already cached, and if not, it queries the database with Bearer::where('token', $token)->first() and caches the result with Cache::put() using a TTL calculated by the second method.

calculateBearerCacheTTL($bearer): This method determines how long a Bearer token should be cached. It receives only existing Bearer instances (non-existent tokens are not cached). Here is how it works:

  • For tokens with no expiration: Some tokens are permanent. Cache these for an hour. They are unlikely to change, and an hour is long enough to be worth having while still being short enough to forget.

  • For tokens with expiration: The TTL is the time remaining, minus a five-minute buffer. The buffer is the whole point. Cache a token for exactly as long as it is valid and the entry dies at the same instant the token does — and a request landing in that window can be served a token that is live in the cache and dead in the database. Subtracting the buffer guarantees the cache gives up first, so the database always casts the deciding vote.

Both the middleware and the service provider use this trait by calling rememberBearerToken():

// In EnsureTokenIsValidMiddleware
$foundToken = $this->rememberBearerToken($token);

// In LicenseServiceProvider
$bearer = $this->rememberBearerToken($token);

The rememberBearerToken() method encapsulates all the caching logic in one place. The cache key is always bearer:{token}, so both places share the same cached value. The first lookup (usually in the middleware) caches the token and calculates the TTL. The second lookup (in the service provider) hits the cache instantly. No additional database queries.

Why this matters: Imagine 1000 requests per second hitting your API. Without caching, that is 2000 database queries per second (one in middleware, one in provider). With caching, that is maybe 2-3 queries per second for newly authenticated clients, and zero queries for repeat clients within the cache window. That is a dramatic improvement. Your database can breathe.

Edge case handling: If a token is revoked while cached, it remains valid until the cache expires. This is a deliberate trade-off: immediate revocation requires either checking the database on every request (kills performance) or using shorter cache TTLs (more database hits). For most use cases, 60-second cache windows (the minimum) are fast enough that revocation is nearly instant while still providing meaningful performance benefits.