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\Model\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\Model;
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\Model\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\Controller\Api;
use App\Controller\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.
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('/');
}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->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.
'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 = 72003. 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.
<?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 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
{
// clear all session data
}
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,
],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.