Authentication
Authentication
This document describes how to configure and use the authentication system in the Hazaar Framework. The system is designed to be highly flexible, decoupling how you check a password (Adapter) from where you store the session (Backend) and how the client remembers the session (Transport).
Configuration
The authentication configuration is located in configs/app.php under the auth key.
Basic Configuration Example
// configs/app.php
'auth' => [
'adapter' => \App\Models\Auth::class,
'backend' => \Hazaar\Auth\Session\Backend\Cache::class,
'transport' => \Hazaar\Auth\Session\Transport\Cookie::class,
'cache' => [
'adapter' => 'file',
],
],{
"auth": {
"adapter": "App\\Model\\Auth",
"backend": "Hazaar\\Auth\\Session\\Backend\\Cache",
"transport": "Hazaar\\Auth\\Session\\Transport\\Cookie",
"cache": {
"adapter": "file"
}
}
}[auth]
adapter = "App\\Model\\Auth"
backend = "Hazaar\\Auth\\Session\\Backend\\Cache"
transport = "Hazaar\\Auth\\Session\\Transport\\Cookie"
[auth.cache]
adapter = "file"The configuration consists of three main components:
- Adapter: The logic that connects to your user database. It is responsible for validating credentials and retrieving current user details.
- Backend: The storage mechanism for the session/token. This decides if the session is stored in a file, in memory (Redis/Memcached), or inside the token itself (JWT).
- Transport: How the session ID or token is transferred between the client and server. This is typically via an HTTP Cookie or HTTP headers.
Options for each component are defined in a property that matches the lower-case class name of the component (e.g., cache for the Cache backend, jwt for the JWT backend).
Expiry Configuration and Precedence
Authentication now uses consistent expire and refresh naming across session backends.
auth.expire: Default access token/session lifetime in seconds.auth.refresh: Default refresh token lifetime in seconds.
Each backend can override these defaults with backend-specific values:
auth.jwt.expire/auth.jwt.refreshauth.cache.expire/auth.cache.refreshauth.phpsession.expire/auth.phpsession.refresh
PHPSession and Cache also support backend-specific idle timeout settings (auth.phpsession.idle, auth.cache.idle).
For PHPSession and Cache, idle controls inactivity expiry for access sessions. If refresh is disabled (or no refresh token is available), expire becomes the absolute session lifetime.
Backend-specific values take precedence over auth-level defaults.
'auth' => [
// Global defaults
'expire' => 900,
'refresh' => 604800,
'backend' => \Hazaar\Auth\Session\Backend\JWT::class,
'transport' => \Hazaar\Auth\Session\Transport\Header::class,
// Backend-specific overrides (used instead of auth.expire/auth.refresh)
'jwt' => [
'expire' => 1800,
'refresh' => 2592000,
'passphrase' => 'super-secret-key-CHANGE-ME',
],
],{
"auth": {
"expire": 900,
"refresh": 604800,
"backend": "Hazaar\\Auth\\Session\\Backend\\JWT",
"transport": "Hazaar\\Auth\\Session\\Transport\\Header",
"jwt": {
"expire": 1800,
"refresh": 2592000,
"passphrase": "super-secret-key-CHANGE-ME"
}
}
}[auth]
expire = 900
refresh = 604800
backend = "Hazaar\\Auth\\Session\\Backend\\JWT"
transport = "Hazaar\\Auth\\Session\\Transport\\Header"
[auth.jwt]
expire = 1800
refresh = 2592000
passphrase = "super-secret-key-CHANGE-ME"Header Transport Notes
When using \Hazaar\Auth\Session\Transport\Header:
- Request token defaults:
- Access token header:
Authorization - Access token prefix:
Bearer - Refresh token header:
Authorization - Refresh token prefix:
Refresh
- Access token header:
- Response emission is optional and disabled by default:
emit_headers => false(default): middleware/adapter commit does not write token headers to the response.
emit_headers => true: writes advisory response headers so clients can capture rotated/new tokens.- Advisory output defaults:
advisory_name => X-Auth-Access-Tokenadvisory_refresh_name => X-Auth-Refresh-Tokenadvisory_prefixandadvisory_refresh_prefixdefault to empty strings.
This transport is client-managed, so APIs usually return the TokenPair in JSON and let the client store/replay tokens.
Defining an Adapter
To create an authentication adapter, you must extend Hazaar\Auth\Adapter. This class is the bridge between the framework's authentication system and your user data source (e.g., a MySQL database, LDAP server, or API).
The primary responsibility of the adapter is to implement the queryAuth method. This method acts as a lookup function: given a username (identity), it returns the stored password hash (credential) and user details. The framework then handles the password verification logic securely.
Example Adapter (app/Models/Auth.php)
<?php
declare(strict_types=1);
namespace App\Models;
use Hazaar\Auth\Adapter;
class Auth extends Adapter
{
/**
* Authenticate a user.
*
* @param string $identity The username or email provided by the user during login.
* @return array|bool Returns user data array on success, or false if the user is not found.
*/
public function queryAuth(string $identity): array|bool
{
// Example logic:
// 1. Query your database for the user with the given $identity.
// $user = \App\Models\User::find(['username' => $identity]);
// 2. If not found, return false.
// 3. Return an array structure expected by the framework.
// It is CRITICAL to return the 'credential' key. This is the hashed password
// from your database. The framework will hash the input password and compare it
// to this value.
return [
'identity' => $identity,
// getCredentialHash() is a helper to generate a hash using the configured method
'credential' => $this->getCredentialHash('password123'),
'data' => [
'id' => 1,
'name' => 'Example User',
'email' => '[email protected]',
'role' => 'admin',
],
];
}
}Usage (Login & Logout)
Authentication is typically handled in your controllers.
Auto Refresh
When session auth middleware is enabled in strict mode (->auth()), Hazaar can refresh tokens during a normal protected request and still complete the requested API call. This works with cookie transport and with header transport using Authorization: Refresh <refresh-token> as fallback.
Enabling Sessions On Routes (session() and auth())
Use auth() and session() on either Route or Group objects to attach auth session middleware.
use App\Controllers\Api;
use App\Controllers\Main;
use Hazaar\Application\Router;
use Hazaar\Application\Router\Group;
// Optional session context (guest access allowed)
Router::group('/', function (Group $group) {
$group->get('/', [Main::class, 'index']);
$group->match(['GET', 'POST'], '/login', [Main::class, 'login']);
})->session();
// Protected routes (authentication required)
Router::group('/api', function (Group $group) {
$group->get('/user', [Api::class, 'user']);
})->auth();Behavior summary:
->session(): Loads the adapter and attempts access-token validation when a token is present, then continues even when unauthenticated.->auth(): Requires a valid session. If access-token validation fails, middleware attempts refresh-token fallback before returning401 Unauthorized.- Both methods store the adapter in request attribute
session, which is exposed in controllers as$this->session. - Middleware calls
commit()after controller execution, while the adapter only persists token changes when internal state was modified.
Redirecting instead of a bare 401
->auth()'s plain, empty-body 401 Unauthorized is the right default for JSON APIs, but it's a poor experience for browser-facing pages — a visitor with an expired session just sees a blank page instead of a login screen. See Custom Auth Middleware below for how to wrap Session and redirect to a login route instead, without any framework changes.
Logging In (Main.php / Api.php)
To log a user in, use $this->session->authenticate($username, $password).
public function login(): ?Response
{
if ($this->request->isPost()) {
$username = $this->request->get('username');
$password = $this->request->get('password');
// Attempt authentication
$result = $this->session->authenticate($username, $password);
if ($result->success) {
return $this->response->redirect('/dashboard');
}
$this->view->set('error', 'Invalid username or password');
}
$this->view('login');
}API Token Response (Standardized)
After authentication, use the Result object to return tokens in a standard format.
- Check
$result->success. - Return
$result->tokenfor API responses.
$result->token is a Hazaar\Auth\TokenPair object that serializes to JSON automatically. The token and refreshToken values inside that pair are Hazaar\Auth\ExpiringToken objects.
public function loginApi(): \Hazaar\Controller\Response\JSON
{
$username = (string) $this->request->get('username');
$password = (string) $this->request->get('password');
$result = $this->session->authenticate($username, $password);
if (!$result->success) {
return new \Hazaar\Controller\Response\JSON([
'success' => false,
'message' => 'Invalid username or password',
], 401);
}
// TokenPair implements JsonSerializable.
return new \Hazaar\Controller\Response\JSON($result->token);
}Standard token JSON shape:
{
"token": "<access-token>",
"expires_in": 1800,
"refresh_token": "<refresh-token>",
"refresh_expires_in": 2592000
}Logging Out
To log a user out, use $this->session->clear().
public function logout(): Response
{
$this->session->clear();
return $this->response->redirect('/');
}Authenticating an Externally-Verified Identity
If your application verifies an identity through some means other than authenticate() (e.g. validating an API key against another service), use authenticateAs() to mark the current request as authenticated for that identity. This sets the identity/data in memory only — no session token is generated or signed, and nothing is persisted, so it works even on a verify-only adapter that has no signing key configured.
public function init(): void
{
$apiKey = $this->request->header('X-Api-Key');
if ($identity = $this->verifyApiKeyRemotely($apiKey)) {
$this->session->authenticateAs($identity);
}
}Accessing User Session
You can access the authentication session adapter from the session request attribute, or through the session controller helper ($this->session).
The value is an authentication adapter object, not a raw user array. Use adapter methods (for example authenticated(), get('identity'), or toArray()) to inspect session/user data.
public function init(): void
{
// Read the adapter from request attributes.
$session = $this->request->getAttribute('session');
// Pass session payload data to the view.
$this->view->set('user', $session?->toArray() ?? []);
}
public function dashboard(): void
{
// Check if user is authenticated
if (!$this->session?->authenticated()) {
// Handle unauthenticated state
}
}Session Controller Helper
The session controller helper is a thin proxy to the authentication adapter. It is available when the current route has either ->session() or ->auth() middleware attached.
Common helper calls:
$this->session->authenticate($identity, $credential)$this->session->authenticateAs($identity, $data)$this->session->authenticated()$this->session->get('identity')$this->session->toArray()$this->session->clear()
If you have routes where middleware may not be attached, guard helper usage:
public function init(): void
{
if ($this->hasHelper('session')) {
$this->view->set('session', $this->session);
}
}Backends
The backend determines how the session data or authentication token is stored server-side (or client-side in the case of JWT).
1. PHPSession
This is the standard, default PHP session handler. It uses the $_SESSION superglobal and PHP's built-in session management mechanism.
Best for: Traditional monolithic web applications where the server handles HTML rendering and state.
Class: Hazaar\Auth\Session\Backend\PHPSessionConfig Key: phpsession
Configuration Options
name: The session name (passed tosession_name()). Use this to isolate your app's session cookies.expire: Access token lifetime in seconds.refresh: Refresh token lifetime in seconds.idle: Idle timeout for access sessions in seconds (0disables idle timeout).
If refresh is disabled, expire is treated as the absolute session lifetime.
'auth' => [
'backend' => \Hazaar\Auth\Session\Backend\PHPSession::class,
'phpsession' => [
'name' => 'MYAPP_SESSION',
'expire' => 3600,
'refresh' => 86400,
'idle' => 900,
],
],{
"auth": {
"backend": "Hazaar\\Auth\\Session\\Backend\\PHPSession",
"phpsession": {
"name": "MYAPP_SESSION",
"expire": 3600,
"refresh": 86400,
"idle": 900
}
}
}[auth]
backend = "Hazaar\\Auth\\Session\\Backend\\PHPSession"
[auth.phpsession]
name = "MYAPP_SESSION"
expire = 3600
refresh = 86400
idle = 9002. JWT (JSON Web Token)
JWTs allow for stateless authentication. Instead of storing session data on the server, the data is encrypted and signed into a token that is sent to the client. When the client returns the token, the server verifies the signature to trust the data.
Best for: REST APIs, Microservices, and Mobile Applications where you want to avoid storing session state on the server or need to share authentication across multiple domains/services.
Class: Hazaar\Auth\Session\Backend\JWTConfig Key: jwt
Configuration Options
alg: Signing algorithm (default:HS256).issuer: The issuer string to verify token origin (default: current app URL).expire: Access token expiration in seconds (default:3600).refresh: Refresh token expiration in seconds (default:86400).passphrase: The secret key used to sign the token. Change this to a strong, random string.fingerprintKeys: Array of$_SERVERkeys to bind to the token (default:['HTTP_USER_AGENT', 'HTTP_ACCEPT']). This helps prevent token theft by ensuring the token is used by the same type of client that requested it. Also binds refresh tokens to the same fingerprint — see Device Binding and Force Logout below. An empty array opts out of binding for both access and refresh tokens.
'auth' => [
'backend' => \Hazaar\Auth\Session\Backend\JWT::class,
'jwt' => [
'passphrase' => 'super-secret-key-CHANGE-ME',
'expire' => 7200,
],
],{
"auth": {
"backend": "Hazaar\\Auth\\Session\\Backend\\JWT",
"jwt": {
"passphrase": "super-secret-key-CHANGE-ME",
"expire": 7200
}
}
}[auth]
backend = "Hazaar\\Auth\\Session\\Backend\\JWT"
[auth.jwt]
passphrase = "super-secret-key-CHANGE-ME"
expire = 7200Refresh Token Storage
The access token is a signed JWT, but the refresh token deliberately is not one: it's an opaque, random value that's rotated (a new one issued, the old one discarded) on every use, and looked up server-side against a store on redemption. This is what makes a refresh token revocable — unlike the access token, which is only ever proven valid by its signature and can't be individually invalidated before it expires.
refresh_tokens: The store class name (or an already-constructed instance) implementingHazaar\Auth\Interface\RefreshTokenStore. Defaults toHazaar\Auth\Store\File\RefreshTokenStore.refresh_tokens_options: Options array passed to the store's constructor.
Two stores ship with the framework, and both need no app-owned database schema or migration:
Hazaar\Auth\Store\File\RefreshTokenStore(default) — a single file under the application's runtime directory. Since every application already has a runtime directory, this works out of the box with zero configuration. It's local to one instance's runtime directory though, so a token minted by one instance of a horizontally-scaled, load-balanced deployment won't be redeemable via another instance — use theCachestore for that case instead. Options:file(filename within the runtime directory, or an absolute path to store it elsewhere entirely; defaultauth_refresh_tokens.db).Hazaar\Auth\Store\Cache\RefreshTokenStore— backed by the sameHazaar\Cache\Adaptersystem used by theCachesession backend below (Redis, Memcached, APC, etc.). Suited to load-balanced deployments that already run a shared cache backend, since a token minted by one instance stays redeemable via any other instance sharing that backend. Options:backend(cache backend name(s), defaults to the application's configuredcachebackend) andoptions(passed through to the cache adapter).
If your application needs durable, queryable refresh-token storage (for example a database-backed audit trail), implement Hazaar\Auth\Interface\RefreshTokenStore yourself against your own schema and migration — the framework deliberately doesn't ship a database-backed store, since it has no way to own a migration for a table that lives in your application's database. A "log out this device" feature doesn't need this though — both shipped stores already support it, see below.
'auth' => [
'backend' => \Hazaar\Auth\Session\Backend\JWT::class,
'jwt' => [
'passphrase' => 'super-secret-key-CHANGE-ME',
// Use the shared Cache store instead of the default File store, e.g. for a
// load-balanced deployment.
'refresh_tokens' => \Hazaar\Auth\Store\Cache\RefreshTokenStore::class,
'refresh_tokens_options' => [
'backend' => 'redis',
],
],
],{
"auth": {
"backend": "Hazaar\\Auth\\Session\\Backend\\JWT",
"jwt": {
"passphrase": "super-secret-key-CHANGE-ME",
"refresh_tokens": "Hazaar\\Auth\\Store\\Cache\\RefreshTokenStore",
"refresh_tokens_options": {
"backend": "redis"
}
}
}
}A refresh token can be revoked before it naturally expires via Adapter::revokeRefreshToken(string $token) (e.g. on logout) or Adapter::revokeAllSessions() (e.g. on password change, to log out every other device). Both are no-ops that return false if the configured session backend doesn't implement Hazaar\Auth\Interface\RevocableSessionBackend — JWT does; PHPSession and Cache don't, since they have no store to revoke against.
Device Binding and Force Logout
Every refresh token is bound to the device it was issued to using the same fingerprintKeys-derived fingerprint as the access token. Redeeming a refresh token from a device with a different fingerprint is a hard rejection — the token is consumed either way (so it can't be replayed), and the caller has to fully re-authenticate. This trades off a small amount of legitimate friction (e.g. a browser update that changes its User-Agent forces a re-login) for denying a stolen refresh token any value outside the device it was stolen from.
Each refresh token also carries a sessionId that identifies the device/session itself, distinct from the token value: a fresh login gets a new sessionId, but every subsequent rotation of that session's refresh token carries the same sessionId forward. This is what makes an outstanding session listable and individually revocable even though the underlying token rotates on every use.
Adapter::listDevices(): array<Hazaar\Auth\DeviceSession>— every outstanding device/session for the currently authenticated identity. EachDeviceSessioncarries asessionId,expiresAt, and aHazaar\Auth\DeviceInfo(platform,browser,userAgent,ipAddress,createdAt,lastSeenAt) — enough to show a user something like "Chrome on Windows, last active 2 hours ago".Adapter::revokeDevice(string $sessionId): bool— logs out one specific device/session by itssessionId, without touching the others.
Both are no-ops (an empty array / false) if the configured session backend doesn't implement Hazaar\Auth\Interface\DeviceListableSessionBackend, or if its refresh token store doesn't implement Hazaar\Auth\Interface\DeviceAwareRefreshTokenStore — JWT implements the former, and both shipped stores (Store\File\RefreshTokenStore and Store\Cache\RefreshTokenStore) implement the latter, so this works out of the box with either default.
foreach ($auth->listDevices() as $session) {
printf(
"%s on %s -- last active %s\n",
$session->device->browser,
$session->device->platform,
date('Y-m-d H:i', $session->device->lastSeenAt)
);
}
// Log out one specific device, leaving the others signed in.
$auth->revokeDevice($sessionIdFromTheListAbove);Platform/browser detection is done by the small, dependency-free Hazaar\Util\UserAgent decoder — reusable anywhere you need to interpret a User-Agent string, not just here.
3. Cache
This backend uses the Hazaar Cache system (Redis, Memcached, File, etc.) to store session data. It offers better performance and control than standard PHP sessions, especially for distributed systems where multiple servers need access to the same session data.
Best for: High-performance web applications or load-balanced environments where you need centralized session storage but want to avoid the limitations of file-based PHP sessions.
Class: Hazaar\Auth\Session\Backend\CacheConfig Key: cache
Configuration Options
adapter: Cache adapter to use (e.g.,file,redis,memcached- default:file).usePragma: (default:false).expire: Access token lifetime in seconds (default:3600).refresh: Refresh token lifetime in seconds (default:86400).idle: Idle timeout for access sessions in seconds (0disables idle timeout).path: Storage path if using thefileadapter (default:runtime/cache/sessions).
If refresh is disabled, expire is treated as the absolute session lifetime.
'auth' => [
'backend' => \Hazaar\Auth\Session\Backend\Cache::class,
'cache' => [
'adapter' => 'redis',
'expire' => 3600,
'refresh' => 86400,
'idle' => 900,
],
],{
"auth": {
"backend": "Hazaar\\Auth\\Session\\Backend\\Cache",
"cache": {
"adapter": "redis",
"expire": 3600,
"refresh": 86400,
"idle": 900
}
}
}[auth]
backend = "Hazaar\\Auth\\Session\\Backend\\Cache"
[auth.cache]
adapter = "redis"
expire = 3600
refresh = 86400
idle = 900Transports
The transport determines how the authentication token or session ID is exchanged between the client and the server for each request.
1. Cookie
Stores the session ID or token in a standard HTTP cookie. The browser automatically handles sending this cookie with every request to the domain.
When using strict auth routes (->auth()), refresh-token re-authentication is handled automatically for cookie transport. If the access token is missing or expired but the refresh cookie is valid, middleware refreshes the session and commits new tokens in the response.
On optional session routes (->session()), requests are allowed to continue without requiring refresh-token fallback.
Best for: Traditional web applications and websites. It is the most secure option for browsers when configured with HttpOnly and Secure flags, as it protects the token from XSS attacks.
Class: Hazaar\Auth\Session\Transport\CookieConfig Key: cookie
Configuration Options
cookie_name: Name of the cookie (default:hazaar-auth-token).expires: Access-token cookie lifetime hint in seconds (default:900).refresh: Refresh-token cookie lifetime hint in seconds (default:2592000).path: Cookie path (default:/).domain: Cookie domain. Set this if you need the cookie to be available on subdomains.secure: Iftrue, the cookie is only sent over HTTPS (default:true).httponly: Iftrue, the cookie cannot be accessed by JavaScript, preventing XSS token theft (default:true).samesite: SameSite policy to prevent CSRF (default:Lax).
'auth' => [
'transport' => \Hazaar\Auth\Session\Transport\Cookie::class,
'cookie' => [
'cookie_name' => 'myapp_auth',
'secure' => true,
'samesite' => 'Strict',
],
],{
"auth": {
"transport": "Hazaar\\Auth\\Session\\Transport\\Cookie",
"cookie": {
"cookie_name": "myapp_auth",
"secure": true,
"samesite": "Strict"
}
}
}[auth]
transport = "Hazaar\\Auth\\Session\\Transport\\Cookie"
[auth.cookie]
cookie_name = "myapp_auth"
secure = true
samesite = "Strict"2. Header
Sends the token in an HTTP request header (typically Authorization). This requires the client (e.g., your JavaScript frontend or mobile app) to manually store the token and attach it to every API request.
With strict auth routes (->auth()), header transport supports an automatic fallback path: if Authorization is present but does not use the access prefix (typically Bearer), access token extraction fails and middleware treats the access token as missing. It then attempts refresh-token extraction using the same header with the refresh prefix (typically Refresh).
This allows smart frontend clients to send Authorization: Refresh <refresh-token> on a normal API request when access has expired; middleware can refresh the session, issue new access/refresh tokens, and still complete the requested API call.
On optional session routes (->session()), requests are allowed to continue without requiring refresh-token fallback.
Best for: REST APIs, Single Page Applications (SPAs), and Mobile Apps where you are making AJAX/Fetch requests and cannot rely on cookies (e.g., cross-domain requests).
Class: Hazaar\Auth\Session\Transport\HeaderConfig Key: header
Configuration Options
name: The name of the header to check (default:Authorization).prefix: The prefix expected before the token value (default:Bearer).
'auth' => [
'transport' => \Hazaar\Auth\Session\Transport\Header::class,
'header' => [
'name' => 'Authorization',
// Resulting Setup: "Authorization: Bearer <your-token>"
'prefix' => 'Bearer',
],
],{
"auth": {
"transport": "Hazaar\\Auth\\Session\\Transport\\Header",
"header": {
"name": "Authorization",
"prefix": "Bearer"
}
}
}[auth]
transport = "Hazaar\\Auth\\Session\\Transport\\Header"
[auth.header]
name = "Authorization"
prefix = "Bearer"Extending Functionality
The Hazaar Framework authentication system is designed to be completely modular. If the built-in backends or transports do not meet your needs, you can easily implement your own.
Custom Session Backend
To create a custom backend (e.g., to store sessions in a NoSQL database like MongoDB or DynamoDB), create a class that implements the Hazaar\Auth\Interface\SessionBackend interface.
reset() vs clear()
The adapter returned by Adapter::getInstance() is a singleton that may be reused across multiple, unrelated requests within the same PHP process (e.g. under FrankenPHP worker mode). To prevent one request's identity/session data from leaking into the next, the framework calls reset() on your backend at the end of every request.
reset() must only drop in-memory state — it must never touch persisted storage. Contrast this with clear(), which is a real logout: it's expected to delete the persisted session (destroy the DB/cache record, etc).
assumeIdentity() vs create()
assumeIdentity() is for establishing a session from an identity that has already been verified through some other channel — for example, a service that validates an API key via an RPC call and wants to mark the current request as authenticated for the resulting identity, without issuing a session token the caller has no use for.
Like reset(), it must only touch in-memory state for the current request: no token generation/signing and no writes to persisted storage. This also means it must not require anything a verify-only implementation lacks (e.g. a private signing key) — see Adapter::authenticateAs(), which is the public entry point for this.
<?php
namespace App\Auth\Backend;
use Hazaar\Auth\ExpiringToken;
use Hazaar\Auth\Interface\SessionBackend;
class MongoDBSession implements SessionBackend
{
private array $config;
private ?string $identity = null;
private ?string $token = null;
public function __construct(array $config)
{
$this->config = $config;
// logic to connect to MongoDB
}
public function create(string $identity, array $data = []): ExpiringToken
{
// logic to create a new session record
$this->identity = $identity;
$this->token = 'generated-access-token';
return new ExpiringToken($this->token, (int) ($this->config['expire'] ?? 3600));
}
public function assumeIdentity(string $identity, array $data = []): void
{
// set identity/data in memory ONLY -- no token, nothing persisted
$this->identity = $identity;
$this->data = $data;
}
public function load(string $token, ?array &$sessionData = null, string $tokenType = 'access', ?string $passphrase = null): bool
{
// logic to load session data by token
$this->token = $token;
$this->identity = 'user_id';
$sessionData = ['identity' => $this->identity];
return true;
}
public function getToken(): ?ExpiringToken
{
return $this->token
? new ExpiringToken($this->token, (int) ($this->config['expire'] ?? 3600))
: null;
}
public function getRefreshToken(): ?ExpiringToken
{
// return refresh token when supported
return null;
}
public function refresh(string $token): bool
{
// refresh existing session/token
return false;
}
public function getIdentity(): ?string
{
// return current identity
return 'user_id';
}
public function isEmpty(): bool
{
// check if session is empty
return false;
}
public function has(string $key): bool
{
// check if key exists in session data
return isset($this->data[$key]);
}
public function get(string $key): mixed
{
// get value from session data
return $this->data[$key] ?? null;
}
public function set(string $key, mixed $value): void
{
// set value in session data
}
public function unset(string $key): void
{
// remove value from session data
}
public function clear(): void
{
// logout: delete the persisted session record (e.g. remove the MongoDB document)
}
public function reset(): void
{
// end of request: drop in-memory state ONLY, leave the persisted record alone
$this->identity = null;
$this->token = null;
}
public function read(): array
{
// return all session data
return [];
}
}Then, configure your application to use this new class:
// configs/app.php
'auth' => [
'backend' => \App\Auth\Backend\MongoDBSession::class,
'mongodbsession' => [ // Configuration for your class
'collection' => 'user_sessions',
],
],Custom Session Transport
To create a custom transport (e.g., to read the token from the query string or a custom encoded payload), create a class that implements the Hazaar\Auth\Interface\SessionTransport interface.
<?php
namespace App\Auth\Transport;
use Hazaar\Auth\ExpiringToken;
use Hazaar\Auth\Interface\SessionTransport;
use Hazaar\Controller\Response;
class QueryString implements SessionTransport
{
private array $config;
public function __construct(array $config)
{
$this->config = $config;
}
public function extractToken(): ?string
{
// logic to extract token from request
return $_GET['token'] ?? null;
}
public function extractRefreshToken(): ?string
{
// logic to extract refresh token from request
return $_GET['refresh_token'] ?? null;
}
public function persistToken(Response $response, ExpiringToken $token, array $options = []): void
{
// logic to add token to response (usually not needed for QueryString transport as it's inbound only,
// but maybe you redirect with it)
}
public function persistRefreshToken(Response $response, ExpiringToken $refreshToken, array $options = []): void
{
// logic to add refresh token to response
}
public function clearToken(Response $response, array $options = []): void
{
// logic to clear token (client side action usually)
}
}Then, configure it in app.php:
// configs/app.php
'auth' => [
'transport' => \App\Auth\Transport\QueryString::class,
],Custom Auth Middleware
->auth() and ->session() attach Hazaar\Auth\Middleware\Session to the route (or route group). On failure, Session returns a plain 401 Unauthorized response with an empty body directly — it does not throw an exception, so a global error handler cannot intercept it. That's the right default for JSON APIs, but a browser-facing page usually wants to redirect the visitor to a login screen instead of showing them a blank page.
Rather than reimplementing token/session validation, wrap Session in your own class implementing Hazaar\Middleware\Interface\Middleware. Delegate to Session::handle(), and only change what happens when it returns Unauthorized:
<?php
namespace App\Middleware;
use Hazaar\Application\Request;
use Hazaar\Auth\Adapter;
use Hazaar\Auth\Middleware\Session;
use Hazaar\Controller\Response;
use Hazaar\Controller\Response\HTTP\Redirect;
use Hazaar\Controller\Response\HTTP\Unauthorized;
use Hazaar\Middleware\Interface\Middleware;
class WebAuth implements Middleware
{
private Session $session;
public function __construct(Adapter $adapter)
{
$this->session = new Session($adapter);
}
public function handle(Request $request, callable $next, mixed ...$args): Response
{
$response = $this->session->handle($request, $next, ...$args);
return $response instanceof Unauthorized ? new Redirect('/login') : $response;
}
}Use it in place of ->auth() on the routes that render pages, and keep ->auth() (plain 401) on your JSON /api/* routes so API clients still get a status code they can branch on rather than a redirect:
use App\Middleware\WebAuth;
use Hazaar\Application\Router;
use Hazaar\Application\Router\Group;
use Hazaar\Auth\Adapter;
// Browser-facing pages: redirect to /login on a missing/expired session.
// A route group means WebAuth is only declared once for every route inside it.
Router::group('/dashboard', function (Group $group) {
$group->get('', [Dashboard::class, 'index']);
$group->get('/settings', [Dashboard::class, 'settings']);
})->middleware(new WebAuth(Adapter::getInstance()));
// JSON API: keep the framework default (plain 401).
Router::group('/api', function (Group $group) {
$group->get('/user', [Api::class, 'user']);
})->auth();Because ->middleware() accepts any Middleware instance — not just the framework's built-ins — this same wrap-and-adapt pattern works for anything you want layered around Session's result (or around any other middleware), not just auth redirects.
Summary
The Hazaar Authentication system provides a robust and flexible foundation for securing your application.
- Decoupled Architecture: By separating the identity check (Adapter), storage (Backend), and delivery (Transport), you can mix and match components to suit any architecture, from simple monoliths to complex microservices.
- Flexible Adapters: The
Adapteris where the "real work" happens. Because you implement thequeryAuthmethod yourself, you are not tied to a specific database table or structure. You can authenticate users against:- Relational Databases (MySQL, PostgreSQL)
- NoSQL Databases
- LDAP / Active Directory
- External REST APIs / OAuth Providers
- Static configuration files
- Extensible: With the ability to implement custom interfaces, the system can grow with your application's requirements.
By leveraging these components, you can ensure your application is secure while maintaining the flexibility to adapt to future infrastructure changes.