Ryan Chandler's bearer package provides a minimal token authentication system: a base Token model, secure token hashing, expiration handling, and a VerifyBearerToken middleware. It handles the fundamental security need: lookup a token from the Authorization: Bearer ... header, verify it exists in the database, and authenticate the request. That's it. No user concepts, no sessions, no relationships to a User model. Perfect for service-to-service APIs.
But the base Token model is intentionally generic:
token: The hashed token stringdomains: AsArrayObject:classexpires_at: Expiration timestamp- Timestamps (created_at, updated_at)
This covers basic token validation but nothing more. For our multi-tenant API, we need additional fields that the package doesn't assume. Extending the base Token model is the right approach: we inherit all the security hardening from the package (hashing, validation, middleware) but add our own properties specific to this API's needs.
Here's why we extend instead of building from scratch:
Security Hardening: The package handles token hashing correctly using bcrypt (secure, salted, timing-safe). Building this ourselves introduces risk.
Proven Middleware: VerifyBearerToken middleware from the package is battle-tested. We extend it with custom domain validation logic rather than reinventing authentication.
Convention: The package follows Laravel patterns. Our custom Bearer model uses standard Eloquent features (casts, relationships, factories).
Minimal API Surface: The package is small (one model, one middleware). We're not fighting a complex framework. We're extending simplicity.
Here's our custom Bearer model extending the package's base Token:
// app/Models/Bearer.php
final class Bearer extends RyanChandler\Bearer\Models\Token
{
protected function casts(): array
{
return [
'settings' => 'array',
];
}
}
Four properties actually matter on our Bearer model:
token (inherited from package): The actual token string. The package hashes this with bcrypt on creation. When a client sends Authorization: Bearer abc123xyz, the package extracts abc123xyz and checks it against the hashed version stored here. This is secure because we never store or compare plain tokens; only hashes are persisted.
expires_at (inherited from package): When the token dies. The package includes this field; we just ensure it's cast to a DateTime object so we can do comparisons like $token->expired and $token->expires_at->diffInSeconds(now()). Tokens are your revocation mechanism: let them expire rather than actively deleting them.
domains (inherited from package): Optional list of allowed domains. The package doesn't include this. We add it to restrict token usage to specific origins. If Partner A's token only works from partner-a.com, an attacker who steals the token from attacker.com gets rejected. Multi-partner security in one field.
settings (custom field, we add this): JSON property holding per-token configuration. The package doesn't include this either. This is where multi-tenancy lives. Each token carries its own driver choice and credentials:
{
"license": {
"driver": "statamic",
"token": "statamic_abc123_secret"
}
}
Partner A's token says "use Statamic with my credentials." Partner B's token says "use Statamic with their credentials." Same API, different backends, no tenant tables, no complex routing. Configuration is baked into the token.
created_at, updated_at (inherited from package): Timestamps. Standard Eloquent behavior. Useful for auditing.
Why This Design?
The package gives us security. We add flexibility. By extending rather than replacing, we get:
- Security by inheritance: Hashing, timing-safe comparisons, proven patterns
- Flexibility by extension: Custom fields for our specific requirements
- Testability: Factories create Bearer tokens with various configurations
- Auditability: Timestamps track when tokens were created
When you create a Bearer token in code or tests, you're creating an instance of our custom model, which automatically inherits all the hashing and validation behavior from the package:
// In code or tests
$bearer = Bearer::factory()
->withDomains(['https://partner-a.com'])
->create();
// Behind the scenes:
// - Package hashes the raw token
// - Our custom model adds domains and settings
// - Factories handle realistic test data
The package handles "is this token real?" Our model handles "what's this token allowed to do?"