laika-framework

Authentication

The Auth module of laikait/laika-engine provides three kinds of guard, resolved by name through Laika\Engine\Auth\AuthManager:

Driver Stores Use for
session The logged-in user’s array in the session Web logins
cookie An opaque token in a cookie “Remember me”
token Hashed bearer tokens in the auth_tokens table APIs, mobile apps

Guards don’t check passwords or protect routes by themselves — you verify credentials (with Vault::verifyPassword()) and call the guard from a controller or pipeline.

Configuration

Guards are defined in lf-config/auth.php, keyed directly by guard name (not wrapped in a 'guards' key):

use App\Model\UsersModel;
use App\Model\StaffsModel;

return [
    'web'      => ['driver' => 'session', 'provider' => 'web'],
    'remember' => ['driver' => 'cookie',  'provider' => 'remember'],
    'admin'    => ['driver' => 'token',   'provider' => StaffsModel::class],
    'user'     => [
        'driver'     => 'token',
        'provider'   => UsersModel::class,
        'connection' => 'default', // where auth_tokens lives
        'install'    => false,     // true creates auth_tokens on first use — development only
    ],
];

driver is required: a missing or unknown one throws InvalidArgumentException("Unknown auth driver […]") when the guard is resolved.

What provider means depends on the driver:

Driver provider
session Optional. The session scope the user is stored in (web → the WEB scope). Without it, the APP scope.
cookie Optional, and currently unused — the cookie name comes from the guard name.
token Required. A Laika\Engine\Model\Model subclass; validateToken() loads the user with its find(). Anything else throws AuthException.

When given, provider must be a non-empty string for every driver, or resolving the guard throws AuthException.

Set connection explicitly on token guards. Without it, tokens are read and written on the default connection, but install creates the table on the connection named default — two different connections if you changed the default with Connection::setDefault().

Resolving a Guard

use Laika\Engine\Auth\AuthManager;

$auth  = new AuthManager();        // reads config('auth')
$guard = $auth->guard('web');

AuthManager::__construct(?array $config = null) reads config('auth') unless you pass an array. guard(string $name) returns a SessionGuard, CookieGuard or TokenGuard, cached per manager; an unknown guard name throws InvalidArgumentException("Guard [api] not configured.").

AuthManager has no constructor dependencies, so you can inject it into controllers and pipelines: public function __construct(private AuthManager $auth) {}.

Session Guard

The session guard needs a session driver configured.

$guard = $auth->guard('web');

$guard->login(['id' => $user['id'], 'email' => $user['email']]);
$user = $guard->user();   // the array you stored, or null
$guard->logout();
Method  
login(array $user): void Store the user array
user(): ?array The stored array, or null
logout(): void Remove it

The user is stored as Session::scope($provider ?? 'APP')->set("laika_auth_{$guard}", $user). There’s no check() or id() — use user() !== null and user()['id'].

A Login Controller

namespace App\Controller;

use App\Model\UsersModel;
use Laika\Engine\Auth\AuthManager;
use Laika\Engine\App\Template;
use Laika\Engine\Services\{Request, Redirect, Vault};
use Laika\Engine\Session\Session;

class LoginController
{
    public function __construct(private AuthManager $auth) {}

    public function show(): string
    {
        return (new Template())->view('auth/login');
    }

    public function login(): ?string
    {
        if (!Request::validate(['email' => 'required|email', 'password' => 'required'])) {
            return $this->show();
        }

        $user = (new UsersModel())->where(['email' => Request::input('email')])->first();

        if (!$user || !Vault::verifyPassword(Request::input('password'), $user['password'])) {
            Request::addError('email', 'These credentials do not match our records.');
            return $this->show();
        }

        Session::regenerate();   // new session id on login
        $this->auth->guard('web')->login(['id' => $user['id'], 'email' => $user['email']]);

        Redirect::to('dashboard');
    }

    public function logout(): void
    {
        $this->auth->guard('web')->logout();
        Session::regenerate();
        Redirect::to('login');
    }
}

Input is HTML-encoded. Request::input('password') returns the encoded value (& → &amp;). Hash and verify through the same path — both with Request::input(), or both with a raw source — or passwords containing & < > " ' won’t match. See Requests.

Stores a token in a cookie named laika_remember_{guard}, typically for “remember me”:

$guard = $auth->guard('remember');

$guard->remember($token);            // 30 days by default
$guard->remember($token, ttl: 86400);
$token = $guard->token();            // null if not set
$guard->forget();
Method  
remember(string $token, int $ttl = 2592000): void Set the cookie (httponly, SameSite=Strict, secure over HTTPS)
token(): ?string Read it, exactly as stored; null if absent
forget(): void Delete it

The guard only stores the token — generating it (Vault::token()), saving a hash of it against the user, and checking it on return are up to you.

Token Guard

Issues, validates and revokes bearer tokens stored in the auth_tokens table. Only a SHA-256 hash of each token is stored.

$guard = $auth->guard('user');

// Issue — send 'token' and 'refresh_token' to the client once; neither can be recovered later
$issued = $guard->issueToken($user['id'], ttl: 3600);
// ['token' => 'b7e1…', 'hashed' => '9f2c…', 'refresh_token' => '4a0d…']

// Validate — returns the user row (from the provider model's find()), or null
$user = $guard->validateToken($plainToken);

// Refresh — revokes the old token and returns a new pair, or null
$issued = $guard->refreshToken($refreshToken, ttl: 3600);

// Revoke
$guard->revoke($plainToken);
$guard->revokeAllForUser($user['id']);
Method  
issueToken(int $userId, ?int $ttl = null): array New token. Without $ttl the token never expires.
validateToken(?string $token, ?int $ttl = null, bool $strict = false): ?array The user, or null if the token is unknown, revoked, expired, or the user is gone
refreshToken(string $refreshToken, ?int $ttl = null): ?array Revoke the token and issue a new pair (same shape as issueToken()), or null for an unknown, revoked or already-used refresh token. Pass the $ttl you issue with — null never expires.
revoke(string $plainToken): bool Revoke one token
revokeAllForUser(int $userId): bool Revoke every token of a user for this guard

The auth_tokens Table

Not created by php laika app:migrate. Either set 'install' => true on the guard once (development), or create it with a privileged connection:

(new \Laika\Engine\Auth\Schema\AuthSchema('default'))->up();
Column Type
id BIGINT auto-increment primary key
user_id BIGINT UNSIGNED
guard VARCHAR(50)
browser, ip VARCHAR(50), nullable
user_agent VARCHAR(255), nullable
token VARCHAR(255), unique — the SHA-256 hash
refresh_token VARCHAR(255), nullable — the SHA-256 hash
expires_at, revoked_at TIMESTAMP, nullable
created_at TIMESTAMP, default now

Plus an index on (user_id, guard). Leave install off in production: it runs on every request that resolves the guard, and needs DDL rights the runtime database user shouldn’t have.

Protecting Routes

A pipeline that requires a logged-in web user:

namespace App\Pipeline;

use Laika\Engine\Auth\AuthManager;
use Laika\Engine\Route\Contracts\PipelineInterface;
use Laika\Engine\Services\Redirect;

class Authenticate implements PipelineInterface
{
    public function __construct(private AuthManager $auth) {}

    public function handle(callable $next, array &$params): ?string
    {
        $user = $this->auth->guard('web')->user();

        if ($user === null) {
            Redirect::to('login');
        }

        $params['user'] = $user;   // available to the controller as $user
        return $next();
    }
}

A pipeline that requires a valid bearer token:

namespace App\Pipeline;

use Laika\Engine\Auth\AuthManager;
use Laika\Engine\Route\Contracts\PipelineInterface;
use Laika\Engine\Services\{Request, Response};

class ApiAuth implements PipelineInterface
{
    public function __construct(private AuthManager $auth) {}

    public function handle(callable $next, array &$params): ?string
    {
        $token = preg_replace('/^Bearer\s+/i', '', Request::header('Authorization') ?? '');
        $user  = $this->auth->guard('user')->validateToken($token ?: null);

        if ($user === null) {
            Response::setStatus(401)->setContentType('application/json');
            return json_encode(['message' => 'Unauthenticated.']);
        }

        $params['user'] = $user;
        return $next();
    }
}
use Laika\Engine\Route\Handler;
use Laika\Engine\Services\Url;

Handler::registerGroup('api', function () {
    Url::get('/me', 'Api\MeController@show');
}, ['ApiAuth']);

Behind Apache with PHP-FPM, Request::header('Authorization') still works — see Deployment.

What the Auth module Doesn’t Do

Errors

Exception Thrown when
InvalidArgumentException guard() with a name not in lf-config/auth.php, or a guard whose driver is missing or unknown
Laika\Engine\Auth\Exceptions\AuthException A misconfigured guard: an empty guard name, a provider that isn’t a non-empty string, or a token guard whose provider isn’t a Laika\Engine\Model\Model subclass
Database exceptions Any token guard method, when the database fails

Validation failures don’t throw — user(), token(), validateToken() and refreshToken() return null. To turn one into a 401 from a pipeline, throw Laika\Engine\Exceptions\AuthenticationException; the framework’s error handler renders it — see Errors & Logging.

See Also