Warlock Authentication
Warlock Authentication
This guide explains how to enable and use authentication for the Warlock server.
Warlock supports four authentication methods:
none(default)list(development use only)basic(standalone, file-backed user database)script(custom external authentication logic)
Where Authentication Is Configured
Authentication is configured under the Warlock server client.auth section.
<?php
return [
'client' => [
'auth' => [
'method' => 'none',
],
],
];{
"client": {
"auth": {
"method": "none"
}
}
}[client.auth]
method = noneWhat Client Authentication Protects
When client.auth uses any method other than none, a client must authenticate before the server will accept:
SUBSCRIBE,UNSUBSCRIBEandTRIGGER(real-time signalling)- admin commands (delayed/exec jobs, service control, KV store)
A client that hasn't authenticated gets an ERROR packet with the reason Authentication required back for any of these, and receives no events. A client can authenticate when it connects (an Authorization: Basic header, which Client::connect($username, $password) sends for you, or a session cookie checked by a script) or afterwards with an AUTH packet, which Client::authenticate() sends. The server answers an AUTH packet with AUTH {result: true, user, role} or AUTH {result: false, reason}.
An authenticated client also has a role. The admin commands need the admin role. The none, list and basic methods give every authenticated client admin. A script gives user unless it returns 'admin'.
$warlock = new Hazaar\Warlock\Client();
$warlock->connect('alice', 'S3curePass!');
$warlock->subscribe('orders.created', fn ($data) => print_r($data));REST triggers (POST /warlock) are checked the same way. Send an Authorization: Basic <base64 username:password> header, or the request is rejected with 401 Unauthorized.
With the none method every client counts as authenticated, so anyone who can reach the server can subscribe to and trigger events.
Authentication Methods
1) None (Default)
Method value: none
This is the default authentication method. It accepts all username/password pairs and is effectively the same as running without authentication.
Use cases:
- local development when security is not needed
- trusted internal test environments
Do not use this method in production.
Example:
<?php
return [
'client' => [
'auth' => [
'method' => 'none',
],
],
];{
"client": {
"auth": {
"method": "none"
}
}
}[client.auth]
method = none2) User List (Development Use Only)
Method value: list
This method stores users directly in configuration (users map of username => password). It is ideal for quick development setup because you only need a text editor.
Why this is development-only:
- credentials are stored in config
- credentials are harder to rotate and audit safely
- there is no CLI-backed user store management for this method
Example:
<?php
return [
'client' => [
'auth' => [
'method' => 'list',
'users' => [
'dev-admin' => 'dev-pass-123',
'qa-user' => 'qa-pass-456',
],
],
],
];{
"client": {
"auth": {
"method": "list",
"users": {
"dev-admin": "dev-pass-123",
"qa-user": "qa-pass-456"
}
}
}
}[client.auth]
method = list
[client.auth.users]
dev-admin = dev-pass-123
qa-user = qa-pass-4563) Basic (Standalone + CLI User Management)
Method value: basic
This method stores users in a Warlock authentication database file (dbFile). It is suitable for standalone environments where you want a simple built-in user store and CLI tooling.
Use this when:
- you want persistent users in a local file
- you want to manage users through the Warlock CLI
- you do not need external identity integration
Example:
<?php
return [
'client' => [
'auth' => [
'method' => 'basic',
'dbFile' => '/var/warlock/auth.db',
],
],
];{
"client": {
"auth": {
"method": "basic",
"dbFile": "/var/warlock/auth.db"
}
}
}[client.auth]
method = basic
dbFile = /var/warlock/auth.db4) Script (Custom/External Authentication)
Method value: script
This method executes a PHP script file and expects the script to return true, false or a role name.
Use this when:
- authenticating against your own database
- integrating external auth systems
- applying custom logic such as account status checks, tenant routing, or IP rules
Configuration example:
<?php
return [
'client' => [
'auth' => [
'method' => 'script',
'scriptPath' => 'configs/warlock-auth.php',
],
],
];{
"client": {
"auth": {
"method": "script",
"scriptPath": "configs/warlock-auth.php"
}
}
}[client.auth]
method = script
scriptPath = configs/warlock-auth.phpDetailed Script Authentication Example
When using script, Warlock includes your script and expects a boolean return value.
Available variables in the script scope:
$username(string): username from the client$password(string): password from the client$client(Hazaar\Warlock\Server\Client|null): current client connection context, if available. Its handshake request headers are available with$client->getHeader('cookie')etc.
The script runs when a client sends credentials, and also once when a client connects without any ($username and $password are both empty strings). That second call lets the script authenticate a client from its handshake headers alone; see Browser Clients. A script that only checks usernames and passwords should return false for empty credentials, as the example below does.
Your script must:
- return
truefor successful authentication, which gives the client theuserrole - return
'admin'to authenticate a trusted client with theadminrole ('user'is the same astrue), see Client Roles - return
falsefor failed authentication
Example script (configs/warlock-auth.php):
<?php
declare(strict_types=1);
use Hazaar\Warlock\Server\Client;
/**
* Warlock script auth contract:
* - $username: string
* - $password: string
* - $client: ?Client
* - return false, true (user role) or 'admin'
*
* This example validates users from a SQL table with password hashes
* and optional client-origin checks.
*/
if (!is_string($username) || '' === trim($username)) {
return false;
}
if (!is_string($password) || '' === $password) {
return false;
}
try {
$pdo = new PDO(
'mysql:host=127.0.0.1;port=3306;dbname=app;charset=utf8mb4',
'app_user',
'app_password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
$stmt = $pdo->prepare(
'SELECT username, password_hash, enabled, expires_at '
.'FROM users '
.'WHERE username = :username '
.'LIMIT 1'
);
$stmt->execute(['username' => $username]);
$user = $stmt->fetch();
if (!$user) {
return false;
}
if ((int) $user['enabled'] !== 1) {
return false;
}
if (!empty($user['expires_at']) && strtotime((string) $user['expires_at']) < time()) {
return false;
}
if (!password_verify($password, (string) $user['password_hash'])) {
return false;
}
// Optional: inspect client details if available in your environment.
if ($client instanceof Client) {
// Add custom client-based checks here if required.
}
// true gives the user role (pub/sub only). Return 'admin' for users allowed the admin commands.
return true;
} catch (Throwable $e) {
// Fail closed on any infrastructure or query error.
return false;
}Browser Clients (warlock.js)
The browser WebSocket API can't set an Authorization header, so a browser authenticates one of two ways.
Session cookie (recommended)
Browsers send cookies with the WebSocket upgrade request. With the script method, the script runs on connect with empty credentials, so it can check the session cookie your application already issues. Nothing secret has to be handed to JavaScript, and nothing ends up in a URL. The Warlock server has to be reachable on a host the cookie is sent to, e.g. the same host behind a reverse proxy, or a subdomain with the cookie's Domain set to cover it.
<?php
declare(strict_types=1);
// configs/warlock-auth.php
if ('' === $username) {
if (null === $client) {
return false;
}
// Browsers send cookies on cross-site WebSocket requests too, so only accept pages from your own site.
if ('https://app.example.com' !== $client->getHeader('origin')) {
return false;
}
if (!preg_match('/(?:^|;\s*)session=([^;]+)/', $client->getHeader('cookie') ?? '', $matches)) {
return false;
}
$userId = my_app_validate_session(urldecode($matches[1])); // Your own session/JWT check.
if (null === $userId) {
return false;
}
$client->username = (string) $userId; // Reported back to the browser and used in server logs.
return true;
}
// Username/password clients (your own PHP code etc.) that need the admin commands.
return my_app_check_password($username, $password) ? 'admin' : false;The browser learns the result from onauth, which fires once the connection is up:
import { createWarlock } from '/libs/js/warlock.js';
const warlock = createWarlock('wss://app.example.com');
warlock.onauth(({ result, user }) => {
if (result) console.log(`Authenticated as ${user}`);
});
warlock.subscribe('orders.created', (data) => console.log(data));Username and password
For pages that hold credentials, e.g. a token issued to the page that the script accepts as a password, call authorise(username, password). It works with every method. Subscriptions and triggers made after the call are held back until the server replies, so they aren't refused. The credentials are sent again after a reconnect.
const warlock = createWarlock('wss://warlock.example.com')
.authorise('alice', 'S3curePass!')
.onauth(({ result, reason }) => {
if (!result) console.error(`Warlock authentication failed: ${reason}`);
});
warlock.subscribe('orders.created', (data) => console.log(data));warlock.authenticated, warlock.user and warlock.role hold the current state.
Client Roles
An authenticated client is either an admin or a user:
admincan do everything: real-time signalling plus the admin commands (SHUTDOWN, delayed/exec jobs, service control and the KV store).usercan only subscribe to and trigger events. Any admin command gets anERRORpacket with the reasonAdmin role required, and the server carries on.
The none, list and basic methods make every authenticated client an admin. Their users are configured by whoever runs the server, so they're trusted.
The script method follows least privilege, because a script is how you authenticate end users such as browsers. Returning true gives the client the user role. Return 'admin' (or set $client->role = ClientRole::ADMIN) for trusted clients that need the admin commands, such as your own PHP code using the KV store or delayed jobs. Services run by an agent talk to the server through the agent's own connection, so they don't need it.
The script can also limit which events the client may use with $client->allowSubscribe and $client->allowTrigger. Each is a list of event IDs, where an entry ending in * matches any event ID with that prefix. null (the default) allows every event, and an empty array allows none. A refused SUBSCRIBE or TRIGGER gets an ERROR packet, and a refused REST trigger gets 403 Forbidden.
<?php
declare(strict_types=1);
// configs/warlock-auth.php
if ('' === $username) {
$userId = my_app_validate_session_cookie($client); // Your own Origin and session check, as above.
if (null === $userId) {
return false;
}
$client->username = (string) $userId;
// This browser may watch its own progress events, and may not trigger anything.
$client->allowSubscribe = ["pricing.progress.{$userId}", 'announcements.*'];
$client->allowTrigger = [];
return 'user';
}
// Trusted PHP clients get the admin role. Returning true here would give them the user role.
return my_app_check_password($username, $password) ? 'admin' : false;The role is included in the AUTH result the browser receives in onauth, and in warlock.role.
Starting Warlock With Authentication
After configuring auth, start the server as normal:
vendor/bin/warlock run -c configs/warlock.jsonManaging Users With the Warlock CLI
The warlock user commands manage users in the file-backed authentication database used by basic auth.
Default DB file:
auth.dbin the current working directory
Use --dbfile (or -f) to specify an explicit path.
List Users
vendor/bin/warlock user list --dbfile /var/warlock/auth.dbAdd User
Non-interactive:
vendor/bin/warlock user add alice --password 'S3curePass!' --dbfile /var/warlock/auth.dbInteractive (prompts for password if omitted):
vendor/bin/warlock user add alice --dbfile /var/warlock/auth.dbDelete User
With confirmation:
vendor/bin/warlock user del alice --dbfile /var/warlock/auth.dbWithout confirmation:
vendor/bin/warlock user del alice --yes --dbfile /var/warlock/auth.dbCheck Credentials
vendor/bin/warlock user check alice 'S3curePass!' --dbfile /var/warlock/auth.dbAgent Access Key
Agents don't use client.auth. They prove who they are with a shared access key instead. Set the same key on the server (agent.accessKey in warlock.json) and on each agent (accessKey in the agent config):
{
"agent": {
"accessKey": "a-long-random-secret"
}
}{
"accessKey": "a-long-random-secret"
}An agent that presents a wrong or missing key is rejected during the handshake with 401 Unauthorized.
If the server has no agent.accessKey, it only accepts agents connecting from a loopback address (127.0.0.0/8 or ::1). If it is listening on any other address, it logs a warning at startup. Set an access key whenever agents run on a different host from the server.
Cluster Access Key
Cluster peers don't use client.auth either. When cluster.enabled is true, every server in the cluster must have the same cluster.accessKey, and the server refuses to start without one. If cluster.name is set, connecting peers must also present the same name:
{
"cluster": {
"enabled": true,
"name": "production",
"accessKey": "another-long-random-secret",
"peers": ["warlock-1.internal:13080"]
}
}A peer that presents a wrong or missing key, or the wrong cluster name, is rejected during the handshake with 401 Unauthorized and isn't added to the cluster. A server with clustering disabled closes peer connections without replying.
Method Selection Guidance
- Use
noneonly when authentication is intentionally disabled (default). - Use
listfor development only, where fast text-editor changes are preferred. - Use
basicfor standalone deployments that need built-in persistent user management. - Use
scriptwhen integrating external/custom authentication systems.
Security Notes
- Avoid storing production credentials in source-controlled config files.
- Always set
agent.accessKeywhen the server listens on a non-loopback address and agents connect from other hosts. - Use a different secret for
cluster.accessKeythan foragent.accessKey. - Prefer
basicorscriptoverlistfor production environments. - If using
script, implement fail-closed behavior and robust input validation. - Only return
'admin'from ascriptfor trusted clients. Limit browser clients' events withallowSubscribeandallowTrigger, so they can't trigger events your services act on. - If a
scriptauthenticates browsers by cookie, check theOriginheader as well. Otherwise any website the user visits can open an authenticated WebSocket to your server with their cookie (cross-site WebSocket hijacking). - Keep your auth DB file and script files readable only by trusted users/processes.