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.