Authentication¶
Note
Not part of core. Install it separately:
composer require kinetis/auth
Bearer/opaque-token authentication: a PSR-15 route middleware that
validates an Authorization: Bearer <token> header and registers the
resolved user on the current request as CurrentUserInterface, plus a
token generator. Storage is entirely up to you — the package has no
opinion on where tokens live.
use Kinetis\Http\Attributes\Get;
use Kinetis\Http\Attributes\Middleware;
use Kinetis\Http\CurrentUserInterface;
use Kinetis\Auth\BearerAuthMiddleware;
#[Middleware(BearerAuthMiddleware::class)]
final readonly class OrderController
{
public function __construct(
private CurrentUserInterface $user,
) {}
#[Get('/orders')]
public function index(): array
{
return ['userId' => $this->user->id()];
}
}
UserProviderInterface¶
The one thing your app implements — resolving a raw token to a user, or
null if it doesn’t match anything:
use Kinetis\Auth\UserProviderInterface;
use Kinetis\Http\CurrentUserInterface;
final readonly class DatabaseUserProvider implements UserProviderInterface
{
public function __construct(
private MysqlConnectionPool $db,
) {}
public function findByToken(string $token): ?CurrentUserInterface
{
$hash = hash('sha256', $token);
$row = new Query($this->db)
->table('users')
->where('token_hash', '=', $hash)
->first(UserRow::class);
return $row;
}
}
Register it once, against the interface:
$app->instance(UserProviderInterface::class, new DatabaseUserProvider($db));
Tip
Store hash('sha256', $token), not the raw token, and look up by that
same hash. Don’t use password_hash()/bcrypt here — a bearer token is
already high-entropy random data, not a low-entropy human password, so a
slow KDF only adds latency to every request’s lookup with no security
benefit.
BearerAuthMiddleware is route middleware only¶
Register it with #[Middleware(BearerAuthMiddleware::class)] on the
controllers or methods that need it — never globally. A health check, a
login endpoint, or /openapi.json needs to stay reachable without a
token, and route middleware only runs after a route has already matched,
so there’s no way for it to block an unmatched request the way global
middleware could.
On a missing, malformed, or unrecognized token it returns 401 directly,
with a WWW-Authenticate: Bearer header, before your controller ever
runs:
{"error": "Unauthenticated."}
On success it does the same thing a hand-written auth middleware would
(see Middleware’s “Registering a value the controller reads later”
section) — $scope->instance(CurrentUserInterface::class, $user) — so any
controller constructor-injecting CurrentUserInterface receives it.
TokenGenerator¶
use Kinetis\Auth\TokenGenerator;
$token = TokenGenerator::generate(); // 64 hex characters, 32 bytes of entropy
A thin wrapper over random_bytes(), hex-encoded so the result is safe to
place directly in an Authorization header with no escaping. Generation
only — issuing a token to a user (verifying a password, calling this,
storing the hash) is your own login endpoint’s job.
See also¶
Middleware —
CurrentUserInterface, the global-vs-route middleware distinction, andRequestScopeself-injection.Persistence —
Query/TransactionGuardfor a database-backedUserProviderInterface.JWT Authentication — stateless JWT verification instead, with no token storage at all.