Authentication
Preflow ships a pluggable authentication system with session-based login, API token guards, password hashing, and PSR-15 middleware. The auth package auto-discovers when config/auth.php exists.
User Model
Your user model must implement the Authenticatable interface. Use AuthenticatableTrait for the common case:
use Preflow\Auth\Authenticatable;
use Preflow\Auth\AuthenticatableTrait;
use Preflow\Data\Model;
use Preflow\Data\Attributes\{Entity, Id, Field};
#[Entity(table: 'users', storage: 'default')]
final class User extends Model implements Authenticatable
{
use AuthenticatableTrait;
#[Id] public string $uuid = '';
#[Field(searchable: true)] public string $email = '';
#[Field] public string $passwordHash = '';
#[Field] public array $roles = [];
#[Field] public ?string $createdAt = null;
}
The trait assumes $uuid, $passwordHash, and $roles properties. Override the trait methods if your schema differs.
Guards
Guards handle the mechanics of authenticating users. Preflow ships two:
SessionGuard
The default guard for web applications. Stores the user ID in $_SESSION['_auth_user_id'] and regenerates the session on login to prevent session fixation.
TokenGuard
For API authentication. Reads Authorization: Bearer <token> headers and looks up the SHA-256-hashed token in a user_tokens table. Stateless -- login() and logout() are no-ops.
Creating a personal access token:
$plain = PersonalAccessToken::generatePlainToken();
$token = new PersonalAccessToken();
$token->uuid = bin2hex(random_bytes(16));
$token->tokenHash = PersonalAccessToken::hashToken($plain);
$token->userId = $user->getAuthId();
$token->name = 'api-key';
$dm->save($token);
// Return $plain to the user (only shown once)
Using Guards
$auth = $container->get(AuthManager::class);
$guard = $auth->guard(); // default guard
$guard = $auth->guard('token'); // named guard
$user = $guard->user($request); // resolve user from request
$guard->login($user, $request); // establish session
$guard->logout($request); // invalidate session
$guard->validate([
'email' => $email,
'password' => $password,
]);
Middleware
AuthMiddleware
Protects routes by requiring an authenticated user. Apply it with the #[Middleware] attribute:
use Preflow\Routing\Attributes\{Route, Get, Middleware};
use Preflow\Auth\Http\AuthMiddleware;
#[Route('/dashboard')]
#[Middleware(AuthMiddleware::class)]
final class DashboardController
{
#[Get('/')]
public function index(ServerRequestInterface $request): ResponseInterface
{
$user = $request->getAttribute(Authenticatable::class);
// ...
}
}
GuestMiddleware
The inverse -- redirects authenticated users away. Use on login and registration pages:
#[Route('/login')]
#[Middleware(GuestMiddleware::class)]
final class LoginController { /* ... */ }
Login and Logout Flow
A typical login controller:
#[Post('/login')]
public function login(ServerRequestInterface $request): ResponseInterface
{
$body = $request->getParsedBody();
$guard = $this->auth->guard();
if (!$guard->validate(['email' => $body['email'], 'password' => $body['password']])) {
// Invalid credentials -- redirect back with error
}
$user = $this->userProvider->findByEmail($body['email']);
$guard->login($user, $request);
return redirect('/dashboard');
}
#[Post('/logout')]
public function logout(ServerRequestInterface $request): ResponseInterface
{
$this->auth->guard()->logout($request);
return redirect('/');
}
Template Functions
The auth package registers template functions for use in Twig:
{% if auth_check() %}
Welcome, {{ auth_user().email }}
<form method="post" action="/logout">
{{ csrf_token()|raw }}
<button type="submit">Logout</button>
</form>
{% endif %}
{% set error = flash('error') %}
{% if error %}
<p class="error">{{ error }}</p>
{% endif %}
| Function | Description |
|---|---|
auth_check() |
Returns true if a user is authenticated |
auth_user() |
Returns the current Authenticatable user or null |
csrf_token() |
Renders a hidden CSRF input field |
flash(key) |
Reads and clears a flash message from the session |
Password Hashing
Preflow uses PHP's native password_hash() and password_verify() with transparent rehash support:
$hasher = $container->get(PasswordHasherInterface::class);
$hash = $hasher->hash('secret');
$hasher->verify('secret', $hash); // true
$hasher->needsRehash($hash); // false (current algorithm)
The hasher automatically upgrades to newer algorithms when needsRehash() returns true.
Configuration
config/auth.php defines guards, user providers, and session settings:
return [
'default_guard' => 'session',
'guards' => [
'session' => [
'class' => Preflow\Auth\SessionGuard::class,
'provider' => 'data_manager',
],
'token' => [
'class' => Preflow\Auth\TokenGuard::class,
'provider' => 'data_manager',
],
],
'providers' => [
'data_manager' => [
'class' => Preflow\Auth\DataManagerUserProvider::class,
'model' => App\Models\User::class,
],
],
'password_hasher' => Preflow\Auth\NativePasswordHasher::class,
'session' => [
'lifetime' => 7200,
'cookie' => 'preflow_session',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
],
];
Custom Guards
Implement GuardInterface for custom authentication:
use Preflow\Auth\{GuardInterface, Authenticatable};
final class LdapGuard implements GuardInterface
{
public function user(ServerRequestInterface $request): ?Authenticatable { /* ... */ }
public function validate(array $credentials): bool { /* ... */ }
public function login(Authenticatable $user, ServerRequestInterface $request): void { /* ... */ }
public function logout(ServerRequestInterface $request): void { /* ... */ }
}
Register it in config/auth.php under the guards key.