Authentication
RestFn comes with token-based authentication built in. A client sends a bearer token, the framework verifies it and turns it into an identity, and actions that require authentication only run when an identity is present.
By default it uses JWT, but every part is swappable.
How it works
Authentication is one middleware and a few services working together:
- The authentication middleware reads the
Authorization: Bearer <token>header from the request. - It passes the raw token to the token parser, which verifies it and returns its payload. The default is a JWT parser.
- It passes the payload to the authenticator, which turns it into an identity. The default reads the identity straight from the token claims.
- The identity is stored on the identity service for the rest of the request.
- When an operation runs an action that's marked as needing authentication, it checks the identity service. If there's no identity, the request is rejected.
The middleware doesn't reject requests without a token. A request with no token just stays unauthenticated, and public actions still run. Authentication is enforced per action, not on the whole request.
Setting it up
The authentication middleware is part of the default stack createDefault() wires
up, so all you have to do is configure the JWT secret:
WebApp::createDefault([
'config' => [
'global' => [
'auth' => ['jwt' => ['secret' => getenv('JWT_SECRET')]],
// ...actions
],
],
])->run();
If you set runner.middleware yourself you replace the default stack, so include
ErrorMiddleware and AuthenticationMiddleware in your list to keep them.
Protecting an action
Mark an action as requiring authentication by implementing
AuthenticatedActionInterface. It's a marker, with no methods to add:
use ArekX\RestFn\Parser\Contracts\ActionInterface;
use ArekX\RestFn\Services\Auth\Contracts\AuthenticatedActionInterface;
class GetProfileAction implements ActionInterface, AuthenticatedActionInterface
{
public function run(mixed $data): array
{
return ['email' => 'me@example.com'];
}
}
When the run (or list) operation is about to run this action and there's no
identity, it throws an AuthenticationRequiredException. An action without the marker
runs whether or not the request is authenticated.
Reading the identity
Your action usually needs to know who's calling. Inject the
IdentityServiceInterface and read the current identity:
use ArekX\RestFn\Services\Auth\Contracts\IdentityServiceInterface;
class GetProfileAction implements ActionInterface, AuthenticatedActionInterface
{
public function __construct(
protected IdentityServiceInterface $identity,
) {}
public function run(mixed $data): array
{
$userId = $this->identity->getIdentity()->getId();
// ...load and return the profile for $userId
}
}
The identity service is shared, so the same instance the middleware writes to is the one your action reads from.
Configuration
All settings are read from configuration. Put them under config.global so every
auth service sees them:
| Key | Default | What it does |
|---|---|---|
auth.jwt.secret |
'' |
Secret used to verify the JWT signature. Required. |
auth.jwt.algorithm |
HS256 |
Algorithm the token must be signed with. |
auth.header |
Authorization |
Header the token is read from. |
auth.scheme |
Bearer |
Scheme prefix before the token. |
auth.identity.idClaim |
sub |
Claim used as the identity id. |
auth.identity.claims |
[] |
Extra claims copied into the identity data. |
The JWT secret has to be long enough for the algorithm (at least 32 bytes for HS256).
'global' => [
'auth' => [
'jwt' => [
'secret' => getenv('JWT_SECRET'),
'algorithm' => 'HS256',
],
'identity' => [
'idClaim' => 'sub',
'claims' => ['email', 'role'],
],
],
],
With the claims above, the identity carries email and role, which you read with
$identity->get('email').
The default authenticator
The default ClaimsAuthenticator builds the identity from the token claims. It reads
the id from auth.identity.idClaim and copies the claims listed in
auth.identity.claims into the identity data. That's enough when everything you need
is already in the token.
If you need to load a user from your database, write your own authenticator. It
implements AuthenticatorInterface and returns your own identity:
use ArekX\RestFn\Services\Auth\Contracts\AuthenticatorInterface;
use ArekX\RestFn\Services\Auth\Contracts\IdentityInterface;
class UserAuthenticator implements AuthenticatorInterface
{
public function __construct(protected UserRepository $users) {}
public function authenticate(mixed $payload): ?IdentityInterface
{
return $this->users->find($payload['sub']); // your IdentityInterface
}
}
Bind it when you create the app:
'aliases' => [
AuthenticatorInterface::class => App\UserAuthenticator::class,
],
Swapping the token format
The token parser is swappable too. To use something other than JWT, bind your own
TokenParserInterface. It takes the raw token string and returns a verified payload,
or throws InvalidTokenException if the token is bad.