enlivenapp/flight-shield

Authentication and authorization for FlightPHP, ported from CodeIgniter Shield

Maintainers

Package info

github.com/enlivenapp/FlightPHP-Shield

Type:flightphp-plugin

pkg:composer/enlivenapp/flight-shield

Statistics

Installs: 10

Dependents: 11

Suggesters: 0

Stars: 0

Open Issues: 0

0.2.1 2026-05-06 00:22 UTC

This package is auto-updated.

Last update: 2026-05-06 00:22:45 UTC


README

Stable? Not Quite Yet License PHP Version Monthly Downloads Total Downloads GitHub Issues Contributors Latest Release Contributions Welcome

Flight Shield

I noticed folks downloading some of these packages. I'm super grateful, Thank You! I would like to let folks know until this notice disappears I'm doing a lot of breaking changes without worrying about them. Once versions are up around 0.5.x things should settle down.

  • additionally, we're waiting on a PR before this package works properly without the AR/fix branch goofiness.

Flight Shield is an authentication and authorization plugin for FlightPHP, ported and adapted from CodeIgniter Shield. It integrates into the flight-school plugin lifecycle and provides session login, access token authentication, HMAC-SHA256 request signing, JWT verification, role-based groups and permissions, password validation, and a full set of route-level middlewares — all wired into FlightPHP with minimal configuration.

Features

  • Session-based authentication (login, logout, remember me with token rotation)
  • Access token authentication (Bearer tokens in the Authorization header)
  • HMAC-SHA256 API authentication with AES-256-GCM encrypted secrets at rest and replay protection
  • JWT authentication (requires firebase/php-jwt)
  • Chain authentication — try multiple authenticators in order, first success wins
  • Groups and permissions (role-based access control with wildcard support)
  • Per-user direct permissions in addition to group-inherited permissions
  • Email two-factor authentication (2FA) as an optional post-login action
  • Email activation on registration as an optional post-register action
  • Magic link login
  • Password validators: composition (min/max length), nothing-personal, dictionary (65k common passwords), and pwned (Have I Been Pwned API)
  • Automatic password rehashing when the cost parameters change
  • Rate limiting on login, 2FA, and magic link endpoints (per-IP, database-backed)
  • CSRF protection on all mutating auth routes via enlivenapp/flight-csrf
  • Force-password-reset flow
  • Login attempt recording (none, failures only, or all)
  • CLI commands for managing users, groups, permissions, and HMAC keys
  • Global helper functions auth() and user_id()
  • View overrides — bundle views can be replaced per-app

Requirements

  • PHP 8.1+
  • flightphp/core ^3.0
  • enlivenapp/flight-school ^0.2
  • enlivenapp/flight-csrf ^0.1
  • enlivenapp/flight-settings ^0.1
  • firebase/php-jwt ^6.0 (optional, required only for JWT authentication)

Installation

1. Install via Composer

composer require enlivenapp/flight-shield

2. Enable the plugin in your flight-school config

In app/config/config.php, add the plugin to the plugins array:

'plugins' => [
    'enlivenapp/flight-shield' => [],
],

On first run, Plugin::register() calls ensureAppConfig(), which inspects your config file and automatically appends hmac and jwt stub entries inside the shield plugin block if they are not already present. This happens transparently — you will see the entries in your config file after the first request.

Shield also uses Flight School's return-array config format in src/Config/Config.php. The defaults from that file are stored on $app under enlivenapp.flight-shield, and the route prefix is defined there with 'routePrepend' => 'auth'.

3. Run the migrations

Shield ships two PHP migration classes under src/Database/Migrations/. Run them via the enlivenapp/migrations runway command:

php runway migrate:single enlivenapp/flight-shield

This creates the following tables: users, auth_identities, auth_logins, auth_token_logins, auth_remember_tokens, auth_groups_users, auth_permissions_users, auth_groups, auth_permissions, auth_group_permissions.

4. Seed default groups and permissions

Default groups, permissions, and group-permission mappings are declared as seed data in Plugin::$seeds. These are applied automatically by enlivenapp/migrations when flight-school calls handleActivate() during plugin activation. There is no separate CLI step — enabling the plugin through the flight-school lifecycle is what fires the seeds.

The seeds populate:

  • auth_groupssuperadmin, admin, user
  • auth_permissionsadmin.access, users.list, users.create, users.edit, users.delete, profile.edit
  • auth_group_permissions — superadmin gets * (all), admin gets the admin/user management permissions, user gets profile.edit

Quick Start

Protect a route with session auth

use Enlivenapp\FlightShield\Middlewares\SessionAuthMiddleware;

Flight::route('/dashboard', function () {
    $user = auth()->user();
    echo 'Hello, ' . $user->username;
})->addMiddleware(new SessionAuthMiddleware(Flight::app()));

Log a user in manually

$result = auth()->attempt(['email' => 'user@example.com', 'password' => 'secret']);

if ($result->isOK()) {
    Flight::redirect('/dashboard');
} else {
    echo $result->reason();
}

Check a permission

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

if ($user->can('users.edit')) {
    // allowed
}

if ($user->inGroup('admin', 'superadmin')) {
    // allowed
}

Get the current user ID anywhere

$id = user_id(); // returns int|string|null

How It Works

Architecture overview

Plugin::register() registers the Auth facade as a Flight service ($app->auth()). The Auth class delegates every call to the currently active AuthenticatorInterface instance, obtained from Authentication::factory(). The factory resolves the alias (session, tokens, hmac, jwt) against the authenticators map in config and returns a singleton per request.

The four authenticators are:

Alias Class Transport
session Session PHP session + optional remember-me cookie
tokens AccessTokens Authorization: Bearer <token> header
hmac HmacSha256 Authorization: HMAC-SHA256 <key>:<sig> + X-Request-Timestamp header
jwt JWT Authorization: Bearer <jwt> header

JWT is not enabled by default (it is commented out in the default authenticators map). Add it explicitly and install firebase/php-jwt to use it.

ChainAuthMiddleware iterates the authentication_chain list (default: ['session', 'tokens', 'hmac']) and stops at the first authenticator that reports loggedIn() === true. This lets a single route accept both browser sessions and API clients.

Authorization uses two parallel systems: groups (assigned via auth_groups_users) and direct permissions (assigned via auth_permissions_users). User::can() checks direct permissions first, then walks the user's groups and queries auth_group_permissions for each. Wildcard permissions (*, users.*) are supported.

Password validation is a pipeline of ValidatorInterface classes run in order. The first failure short-circuits the pipeline and returns an error.

Session Authentication

The Session authenticator stores the authenticated user's ID in $_SESSION[$field] (default key: user). On login, the session ID is regenerated. On logout, the session key and any pending action key are removed and the session is regenerated again.

Remember me

When session.allow_remembering is true (the default), calling auth('session')->getAuthenticator()->remember() after login stores a split-token cookie. The cookie holds a selector:validator pair. The selector is stored in plain text in auth_remember_tokens; the validator is stored as sha256(validator). On each request where no session exists, the remember cookie is checked:

  1. Locate the token by selector.
  2. Compare sha256(cookie_validator) against the stored hash using hash_equals().
  3. If a mismatch is detected (possible token theft), all remember tokens for that user are purged.
  4. On success, the old token is deleted, the user is logged in, and a new token is issued (token rotation).

Expired tokens are purged probabilistically (20% of successful remember-me logins) to avoid a separate cleanup job.

Post-login actions (2FA, email activation)

When actions.login is set to an action class (e.g. Email2FA), attempt() puts the session into a STATE_PENDING state by storing the user ID and action class name in the session. The user is not considered fully logged in until the action is completed. The routes at /auth/2fa and /auth/activate drive this flow.

Configuration

'session' => [
    'field'                => 'user',         // Session key
    'allow_remembering'    => true,
    'remember_cookie_name' => 'remember',
    'remember_length'      => 30 * 86400,     // 30 days
],

Access Token Authentication

The AccessTokens authenticator reads the Authorization header and strips the Bearer prefix. It hashes the raw token with SHA-256, looks it up in auth_identities (type = access_token), and checks expiry and unused_token_lifetime. On success it touches last_used_at.

Tokens are stored as identities in auth_identities. Use shield:user CLI or your own application code to generate them.

HMAC Authentication

The HmacSha256 authenticator authenticates API clients that sign each request rather than sending a reusable secret.

Request format

The client sends:

Authorization: HMAC-SHA256 <userKey>:<signature>
X-Request-Timestamp: <unix timestamp>

The signature is computed as:

HMAC-SHA256(secret, timestamp + "\n" + raw_request_body)

Verification

  1. Parse userKey and signature from the header.
  2. Look up the identity by userKey in auth_identities (type = hmac_sha256).
  3. Reject the request if the timestamp is more than 300 seconds from time() (replay protection).
  4. Decrypt the stored secret2 and recompute the HMAC. Compare with hash_equals().
  5. Check unused_token_lifetime.
  6. Touch last_used_at.

HMAC secrets at rest

Secrets are stored in auth_identities.secret2 encrypted with AES-256-GCM. Encryption keys live in app/config/config.php under hmac.encryption_keys (never in the database). The active key is identified by hmac.encryption_current_key.

Use shield:hmac CLI commands to set up and rotate keys (see CLI Commands section).

JWT Authentication

The JWT authenticator is stateless. It reads Authorization: Bearer <token>, parses and verifies the JWT using JWTManager (which wraps firebase/php-jwt), extracts the sub claim as the user ID, and loads the user from the database.

JWT is not enabled by default. To enable it:

  1. Install the dependency: composer require firebase/php-jwt ^6.0
  2. Add jwt to the authenticators map in your plugin config:
'authenticators' => [
    'session' => \Enlivenapp\FlightShield\Authentication\Authenticators\Session::class,
    'tokens'  => \Enlivenapp\FlightShield\Authentication\Authenticators\AccessTokens::class,
    'hmac'    => \Enlivenapp\FlightShield\Authentication\Authenticators\HmacSha256::class,
    'jwt'     => \Enlivenapp\FlightShield\Authentication\Authenticators\JWT::class,
],
  1. Configure JWT keys (auto-injected as a stub by ensureAppConfig(), fill in the values):
'jwt' => [
    'header'         => 'Authorization',
    'time_to_live'   => 3600,
    'default_claims' => ['iss' => 'https://example.com'],
    'keys' => [
        'default' => [
            ['kid' => '', 'alg' => 'HS256', 'secret' => 'your-256-bit-secret'],
        ],
    ],
],

To generate a token for a user, call generateToken() directly on the JWT authenticator instance:

$jwt = auth('jwt')->getAuthenticator()->generateToken($user);

Chain Authentication

ChainAuthMiddleware tries each authenticator in authentication_chain in order. The first one whose loggedIn() returns true grants access and records the active date. If all fail, the user is redirected to the configured login URL.

use Enlivenapp\FlightShield\Middlewares\ChainAuthMiddleware;

Flight::route('/api/resource', function () {
    // accessible by session, Bearer token, or HMAC
})->addMiddleware(new ChainAuthMiddleware(Flight::app()));

Default chain: ['session', 'tokens', 'hmac']. Override with authentication_chain in config.

Groups and Permissions

Data model

  • auth_groups — group definitions (alias, title, description)
  • auth_permissions — permission definitions (alias, description)
  • auth_group_permissions — maps group aliases to permission aliases
  • auth_groups_users — maps user IDs to group aliases
  • auth_permissions_users — maps user IDs to permission aliases (direct grants)

Permission resolution

User::can(string $permission) checks in this order:

  1. Direct user permissions in auth_permissions_users
  2. Group permissions from auth_group_permissions for each of the user's groups

Wildcard * in a group's permission list matches every permission check. Partial wildcards like users.* match users.create, users.edit, etc.

Superadmin visibility

Superadmin users are hidden from non-superadmin callers by default. This prevents lower-privilege admins from viewing, editing, or deleting superadmin accounts.

The User model methods that support this:

  • findAllPaginated(int $page, int $perPage, bool $includeSuperadmins = false) — when $includeSuperadmins is false, users in the superadmin group are filtered out of results.
  • countAll(bool $includeSuperadmins = false) — same filtering for counts.
  • findById(int $id, bool $includeSuperadmins = true) — when $includeSuperadmins is false, returns null if the target user is in the superadmin group. Defaults to true for backward compatibility with internal auth lookups.

Pass $currentUser->inGroup('superadmin') as the flag so superadmins see everyone and non-superadmins see only non-superadmin users.

Note: Filtering is done post-fetch in PHP (not at the query level) due to ActiveRecord limitations with table-qualified column names in WHERE clauses. For paginated results, this means a page may contain slightly fewer results than $perPage if superadmin users were in the batch. This is a negligible edge case in practice.

Default seeds

When the plugin is activated through flight-school's lifecycle, the following seed data is inserted:

Groups: superadmin, admin, user

Permissions: admin.access, users.list, users.create, users.edit, users.delete, profile.edit

Group-permission mappings:

  • superadmin* (all permissions)
  • adminadmin.access, users.list, users.create, users.edit, users.delete
  • userprofile.edit

Working with groups and permissions in code

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

// Check group membership
$user->inGroup('admin');            // true if in 'admin'
$user->inGroup('admin', 'superadmin'); // true if in either

// Check permission
$user->can('users.edit');

// Modify (requires ORM)
$orm = Flight::app()->orm();
$user->addGroup('editor', $orm);
$user->removeGroup('editor', $orm);
$user->addPermission('posts.create', $orm);

Password Validators

Validators run as a pipeline during registration and password changes. The default set is CompositionValidator, NothingPersonalValidator, DictionaryValidator. PwnedValidator is available but not enabled by default.

Validator What it checks
CompositionValidator Min length (default 8) and max length (128), per NIST SP 800-63B
NothingPersonalValidator Password must not contain or closely match the username, email address, or reversed username. Also checks similarity via similar_text() against max_similarity (default 50%)
DictionaryValidator Password must not appear in the bundled 65,000-entry common-password list
PwnedValidator Password must not appear in the Have I Been Pwned database (uses k-anonymity API, fails open if the API is unreachable)

Configure in passwords:

'passwords' => [
    'algorithm'      => PASSWORD_DEFAULT, // PASSWORD_BCRYPT or PASSWORD_ARGON2ID
    'cost'           => 12,
    'memory_cost'    => 65536,   // Argon2 only
    'time_cost'      => 4,       // Argon2 only
    'threads'        => 1,       // Argon2 only
    'min_length'     => 8,
    'max_similarity' => 50,
    'validators'     => [
        \Enlivenapp\FlightShield\Passwords\CompositionValidator::class,
        \Enlivenapp\FlightShield\Passwords\NothingPersonalValidator::class,
        \Enlivenapp\FlightShield\Passwords\DictionaryValidator::class,
        // \Enlivenapp\FlightShield\Passwords\PwnedValidator::class,
    ],
],

Middlewares

Apply middlewares to routes or route groups using FlightPHP's ->addMiddleware().

SessionAuthMiddleware

Requires an active session login. Redirects to the configured login URL if not authenticated. Records the active date on success.

use Enlivenapp\FlightShield\Middlewares\SessionAuthMiddleware;

Flight::route('/dashboard', function () { ... })
    ->addMiddleware(new SessionAuthMiddleware(Flight::app()));

TokenAuthMiddleware

Authenticates via a Bearer access token in the Authorization header. Halts with a redirect on failure.

use Enlivenapp\FlightShield\Middlewares\TokenAuthMiddleware;

Flight::route('/api/data', function () { ... })
    ->addMiddleware(new TokenAuthMiddleware(Flight::app()));

HmacAuthMiddleware

Authenticates via HMAC-SHA256 request signing. Reads the Authorization header, X-Request-Timestamp header, and the raw request body. Redirects on failure.

use Enlivenapp\FlightShield\Middlewares\HmacAuthMiddleware;

Flight::route('POST /api/webhook', function () { ... })
    ->addMiddleware(new HmacAuthMiddleware(Flight::app()));

JWTAuthMiddleware

Authenticates via a JWT Bearer token. Requires firebase/php-jwt. Redirects on failure.

use Enlivenapp\FlightShield\Middlewares\JWTAuthMiddleware;

Flight::route('/api/secure', function () { ... })
    ->addMiddleware(new JWTAuthMiddleware(Flight::app()));

ChainAuthMiddleware

Tries each authenticator in authentication_chain in order. Grants access on the first success. Redirects to the login URL if all fail.

use Enlivenapp\FlightShield\Middlewares\ChainAuthMiddleware;

Flight::route('/mixed', function () { ... })
    ->addMiddleware(new ChainAuthMiddleware(Flight::app()));

GroupMiddleware

Requires the authenticated user to belong to at least one of the specified groups. Redirects to group_denied URL on failure, login URL if not authenticated.

use Enlivenapp\FlightShield\Middlewares\GroupMiddleware;

Flight::group('/admin', function () { ... }, [
    new GroupMiddleware(Flight::app(), 'admin', 'superadmin'),
]);

PermissionMiddleware

Requires the authenticated user to hold at least one of the specified permissions (checked via User::can()). Redirects to permission_denied URL on failure.

use Enlivenapp\FlightShield\Middlewares\PermissionMiddleware;

Flight::route('/posts/create', function () { ... })
    ->addMiddleware(new PermissionMiddleware(Flight::app(), 'posts.create'));

ForcePasswordResetMiddleware

If the authenticated user's requiresPasswordReset() returns true, redirects to the configured force_reset URL. Passes through silently if the user is not logged in.

use Enlivenapp\FlightShield\Middlewares\ForcePasswordResetMiddleware;

Flight::route('/account', function () { ... })
    ->addMiddleware(new ForcePasswordResetMiddleware(Flight::app()));

RateLimitMiddleware

Applied automatically to POST /auth/login, POST /auth/magic-link, POST /auth/2fa/verify, and POST /auth/2fa/resend. Can also be applied to custom routes.

Counts failed login attempts from the client IP across both auth_logins and auth_token_logins within the decay_minutes window. If max_attempts is reached and the most recent failure is still within the lockout_minutes window, the request is halted with HTTP 429 and a JSON error body.

use Enlivenapp\FlightShield\Middlewares\RateLimitMiddleware;

Flight::route('POST /custom-auth', function () { ... })
    ->addMiddleware(new RateLimitMiddleware(Flight::app()));

CLI Commands

Flight Shield uses the runway CLI provided by flightphp/runway.

shield

Displays available sub-commands.

php runway shield

shield:user

Manage users.

php runway shield:user create      -n admin -e admin@example.com -g superadmin
php runway shield:user list
php runway shield:user activate    -e user@example.com
php runway shield:user deactivate  -n username
php runway shield:user delete      -e user@example.com
php runway shield:user password    -n username
php runway shield:user changename  -n username --new-name newusername
php runway shield:user changeemail -n username --new-email new@example.com
php runway shield:user addgroup    -n username -g admin
php runway shield:user removegroup -n username -g admin

shield:group

Manage groups.

php runway shield:group list
php runway shield:group info             -a admin
php runway shield:group create           -a editor -t Editor -d "Content editors"
php runway shield:group update           -a editor -t "Senior Editor"
php runway shield:group delete           -a editor
php runway shield:group permissions      -a admin
php runway shield:group addpermission    -a editor -p posts.create
php runway shield:group removepermission -a editor -p posts.create

Deleting a group also removes all group-permission mappings and all user memberships. Any user left with no groups after deletion is automatically reassigned to the user group.

shield:permission

Manage permissions.

php runway shield:permission list
php runway shield:permission create -a posts.create -d "Create posts"
php runway shield:permission update -a posts.create -d "Create blog posts"
php runway shield:permission delete -a posts.create

shield:hmac

Manage HMAC encryption keys and token lifecycle.

# Initial setup
php runway shield:hmac init

# Key inspection
php runway shield:hmac listkeys

# Key rotation
php runway shield:hmac addkey
php runway shield:hmac reencrypt     # migrate all secrets to the active key
php runway shield:hmac removekey -k k1

# Bulk operations
php runway shield:hmac encrypt       # encrypt any unencrypted secrets
php runway shield:hmac decrypt       # remove encryption from all secrets
php runway shield:hmac invalidateAll # immediately expire all HMAC tokens

Key rotation workflow:

listkeys → addkey → reencrypt → removekey -k <old-key-id>

Configuration

All options live inside the shield entry in app/config/config.php. The plugin merges these with its defaults, so you only need to include values you want to override.

'plugins' => [
    'enlivenapp/flight-shield' => [
        // ... your overrides here
    ],
],

Full default configuration

Key Default Description
default_authenticator 'session' Authenticator used when none is specified
authentication_chain ['session', 'tokens', 'hmac'] Order tried by ChainAuthMiddleware
allow_registration true Allow new user self-registration
allow_magic_link false Enable magic link login
magic_link_lifetime 3600 Magic link token TTL in seconds
default_group 'user' Group assigned to newly registered users
record_login_attempt 'all' Login attempt recording: 'none', 'failure', or 'all'
record_active_date true Update last_active on every authenticated request
unused_token_lifetime 7776000 Access/HMAC token inactivity TTL in seconds (90 days)
valid_login_fields ['email'] Fields accepted as login identifier
personal_fields [] Additional user fields checked by NothingPersonalValidator
email_sender null Callback for outbound emails (see Email Setup)
actions.login null Post-login action class (Email2FA::class or null)
actions.register null Post-register action class (EmailActivator::class or null)
token_header 'Authorization' Header read by the AccessTokens authenticator
hmac_header 'Authorization' Header read by the HmacSha256 authenticator

Session settings

'session' => [
    'field'                => 'user',
    'allow_remembering'    => true,
    'remember_cookie_name' => 'remember',
    'remember_length'      => 30 * 86400, // 30 days
],

Password settings

'passwords' => [
    'algorithm'      => PASSWORD_DEFAULT,
    'cost'           => 12,
    'memory_cost'    => 65536,
    'time_cost'      => 4,
    'threads'        => 1,
    'min_length'     => 8,
    'max_similarity' => 50,
    'validators'     => [
        \Enlivenapp\FlightShield\Passwords\CompositionValidator::class,
        \Enlivenapp\FlightShield\Passwords\NothingPersonalValidator::class,
        \Enlivenapp\FlightShield\Passwords\DictionaryValidator::class,
    ],
],

HMAC settings

'hmac' => [
    'encryption_keys'        => [],         // populated by shield:hmac init/addkey
    'encryption_current_key' => '',         // alias of the active key
    'encryption_cipher'      => 'aes-256-gcm',
    'secret2_storage_limit'  => 255,
],

JWT settings

'jwt' => [
    'header'         => 'Authorization',
    'time_to_live'   => 3600,
    'default_claims' => ['iss' => ''],
    'keys' => [
        'default' => [
            ['kid' => '', 'alg' => 'HS256', 'secret' => ''],
        ],
    ],
],

Rate limiting settings

'rate_limiting' => [
    'enabled'         => true,
    'max_attempts'    => 10,
    'decay_minutes'   => 30,
    'lockout_minutes' => 30,
],

Redirect URLs

'redirects' => [
    'login'             => '/auth/login',
    'logout'            => '/',
    'after_login'       => '/',
    'after_register'    => '/',
    'after_logout'      => '/auth/login',
    'force_reset'       => '/auth/reset-password',
    'permission_denied' => '/auth/login',
    'group_denied'      => '/auth/login',
],

Routes

Shield registers the following routes under the prefix defined by the returned routePrepend value in src/Config/Config.php (default: auth).

Method Path Notes
GET /auth/login Show login form
POST /auth/login Process login — CSRF + rate limit
GET /auth/logout Log out
GET /auth/register Show registration form
POST /auth/register Process registration — CSRF
GET /auth/magic-link Show magic link request form
POST /auth/magic-link Send magic link email — CSRF + rate limit
GET /auth/magic-link/verify Verify magic link token
GET /auth/2fa Show 2FA verification page (sends code)
POST /auth/2fa/verify Verify 2FA code — CSRF + rate limit
POST /auth/2fa/resend Resend 2FA code — CSRF + rate limit
GET /auth/activate Show activation page (sends email)
GET /auth/activate/verify Verify email activation token

Email Setup

Shield does not ship with a mailer. Provide a callback that accepts the recipient address, subject, and body:

'email_sender' => function (string $to, string $subject, string $body): void {
    mail($to, $subject, $body);
},

The callback is invoked for 2FA codes, magic links, and email activation messages.

Views

Shield renders its own views by default. To override any view, create a matching file in your app:

app/views/enlivenapp/flight-shield/<view-name>.php

Overrideable views:

  • login.php
  • register.php
  • magic_link_login.php
  • magic_link_message.php
  • 2fa_verify.php
  • activate.php

Security Notes

  • Passwords — hashed with password_hash() using PASSWORD_DEFAULT (bcrypt, cost 12). Configurable to Argon2 via algorithm, memory_cost, time_cost, and threads. Passwords are automatically rehashed when cost parameters change.
  • HMAC secrets — stored encrypted with AES-256-GCM. Encryption keys live in app/config/config.php, never in the database.
  • Token comparison — all token and hash comparisons use hash_equals() to prevent timing attacks.
  • HMAC replay protection — requests with a timestamp more than 300 seconds from time() are rejected.
  • Remember-me tokens — split-token scheme (selector stored plain, validator stored as SHA-256 hash). Mismatch purges all tokens for the user.
  • CSRF — all mutating auth routes are protected by CsrfMiddleware from enlivenapp/flight-csrf.
  • Rate limiting — failed login attempts are tracked per IP across auth_logins and auth_token_logins. Excessive failures result in HTTP 429.
  • Session fixation — the session ID is regenerated on login and logout.

License

MIT — see LICENSE.