enlivenapp / flight-shield
Authentication and authorization for FlightPHP, ported from CodeIgniter Shield
Package info
github.com/enlivenapp/FlightPHP-Shield
Type:flightphp-foundation
pkg:composer/enlivenapp/flight-shield
Requires
- php: >=8.1
- ext-openssl: *
- ext-pdo: *
- enlivenapp/flight-csrf: ^0.2
- enlivenapp/flight-sessions: ^0.1
- flightphp/active-record: >=0.7
- flightphp/core: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
- firebase/php-jwt: Required for JWT authentication (^7.0)
Provides
None
Conflicts
None
Replaces
None
README
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.
Flight Shield is an authentication and authorization plugin for FlightPHP. It was ported and adapted from CodeIgniter4 Shield around v1.2. We watch for security patches, and we've made significant functionality additions. It runs inside FlightPHP as used by Pubvana CMS v3. It gives you session login, access token auth, HMAC-SHA256 request signing, JWT verification, role-based groups and permissions, password validation, and route-level middlewares.
Features
- Database-backed sessions with AES-256-GCM encrypted payloads (via
enlivenapp/flight-sessions) - Session-based authentication (login, logout, remember me with token rotation)
- Access token authentication (Bearer tokens in the
Authorizationheader) - 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()anduser_id() - View overrides - bundle views can be replaced per-app
Requirements
- PHP 8.1+
flightphp/core^3.0flightphp/active-record>=0.7 - all Shield models extend\flight\ActiveRecordenlivenapp/flight-csrf^0.2enlivenapp/flight-sessions^0.1 - database-backed session storage (all session state lives in SQL with encrypted payloads)firebase/php-jwt^6.0 (optional, required only for JWT authentication)
Installation
1. Install via Composer
composer require enlivenapp/flight-shield
2. Provide the override config
Shield ships with sensible defaults in src/Config/Config.php. Create the override config file app/config/shield.php to set your own values (details in Configuration). At minimum it sets the route prefix, 'routePrepend' => 'auth', and you can add anything from the default config file to it.
// app/config/shield.php return [ 'routePrepend' => 'auth', // ... your overrides here ];
Start sessions before Shield so session state is ready at auth time. Start CSRF after both.
3. Run the migrations
Shield ships PHP migration classes under Database/Migrations/ (plus seed data in Database/Seeds/). Run all pending migrations across every module with the enlivenapp/migrations runway command:
php runway migrate:all
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. Apps that apply pending migrations during boot pick these up on the first request, so you can skip the CLI step.
4. Seed default groups and permissions
Default groups, permissions, and group-permission mappings are declared as seed data in Plugin::$seeds / the package Seeds directory. They apply automatically when migrations run, so there is no separate CLI step.
The seeds populate:
auth_groups-superadmin,admin,userauth_permissions-admin.access,users.list,users.create,users.edit,users.delete,profile.editauth_group_permissions- superadmin gets*(all), admin gets the admin/user management permissions, user getsprofile.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 }
Note: Superadmins always has access
$user->can(*) = true
Get the current user ID anywhere
$id = user_id(); // returns int|string|null
Management APIs
For app-level administration (user/group/permission CRUD), Shield exposes management services through Auth. These services wrap the models and apply validation and consistency rules, so prefer them over calling the models directly.
| Accessor | Class | Purpose |
|---|---|---|
auth()->users() |
Services\UserManagement |
List/find/create users, profile updates (username/email/password with validation + hashing), activate/deactivate, soft delete |
auth()->groups() |
Authorization\Groups |
Group CRUD, permission assignment/sync, delete with membership fallback to user |
auth()->permissions() |
Authorization\Permissions |
Permission CRUD; deletes also clean group mappings and direct grants/denies |
auth()->stats() |
Services\UserStats |
User/login statistics |
$auth = Flight::auth(); // Superadmin visibility: superadmins see everyone, // Superadmins are hidden from every other user group. $isSuper = $auth->user()?->inGroup('superadmin') ?? false; $result = $auth->users()->create($username, $email, $password, 'editor'); if ($result->isOK()) { $user = $result->extraInfo(); } else { echo $result->reason(); } $auth->users()->paginated($page, 20, $isSuper); $auth->users()->updateProfile($user, ['email' => $newEmail, 'password' => $newPassword]); $auth->groups()->syncPermissions('editor', ['posts.create', 'posts.edit']); $auth->permissions()->create('posts.create', 'Create blog posts');
UserManagement::create(), updateProfile(), and similar operations return a Result (isOK(), reason(), extraInfo()). Password changes run through the configured password validators before hashing; duplicate email addresses are rejected.
How It Works
Architecture overview
Plugin::register() registers Auth as a Flight service ($app->auth()). The Auth class delegates every call to the currently active AuthenticatorInterface instance. That instance comes from Authentication::factory(), which resolves the alias (session, tokens, hmac, jwt) against the authenticators map in config and returns a singleton per request. All models extend flightphp/active-record (\flight\ActiveRecord).
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. That 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. Matching uses hierarchical wildcard semantics: a trailing * grants a node and all its descendants (users.* grants users.create, users.create.team, …), and a middle * matches exactly one segment (admin.*.post). A standalone * matches nothing. Use can() on a user in the superadmin group instead (that group bypasses all permission checks).
Password validation is a pipeline of ValidatorInterface classes run in order. The first failure short-circuits the pipeline and returns an error.
Session Authentication
Shield stores all session state in the database through enlivenapp/flight-sessions, not PHP's native file sessions. See that package's README for how the session storage works.
The Session authenticator stores the authenticated user's ID under $config['session.field'] (default key: user). On login the session ID regenerates and the row binds to the user. On logout the session keys (user, auth_action) are removed, the user binding clears, and the session regenerates 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:
- Locate the token by selector.
- Compare
sha256(cookie_validator)against the stored hash usinghash_equals(). - If a mismatch is detected (possible token theft), all remember tokens for that user are purged.
- 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 by storing the user ID and action class name in the session. The user isn't considered fully logged in until the action completes. 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 the shield:user CLI or your own application code to generate them.
HMAC Authentication
The HmacSha256 authenticator authenticates API clients that sign each request instead of 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
- Parse
userKeyandsignaturefrom the header. - Look up the identity by
userKeyinauth_identities(type =hmac_sha256). - Reject the request if the timestamp is more than 300 seconds from
time()(replay protection). - Decrypt the stored
secret2and recompute the HMAC. Compare withhash_equals(). - Check
unused_token_lifetime. - Touch
last_used_at.
HMAC secrets at rest
Secrets are stored in auth_identities.secret2 encrypted with AES-256-GCM. Encryption keys live in the hmac.encryption_keys override block (never in the database); the active key is identified by hmac.encryption_current_key. These are secrets, so prefer keeping them out of version control. Your host's .env loader can inject them into the config array at boot.
The shield:hmac CLI writes and rotates these keys in place into the same override config file described under Configuration. That file must already contain an empty hmac block. See the shield:hmac section below for the full workflow.
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:
- Install the dependency:
composer require firebase/php-jwt ^6.0 - Add
jwtto theauthenticatorsmap 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, ],
- Provide JWT keys in your config overrides (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 aliasesauth_groups_users- maps user IDs to group aliasesauth_permissions_users- maps user IDs to permission aliases (direct grants)
Permission resolution
User::can(string $permission) checks in this order:
- Direct user permissions in
auth_permissions_users - Group permissions from
auth_group_permissionsfor each of the user's groups
Permission matching is hierarchical. A trailing * matches a node and every descendant (users.* grants users.create, users.edit, users.edit.settings, … but never users itself); a middle * matches exactly one segment (admin.*.post grants admin.news.post but not admin.news.manage.post). Wildcards are only allowed as whole segments, and a standalone * no longer matches everything. Grant the superadmin group to bypass checks entirely.
Superadmin visibility
Superadmin users are hidden from non-superadmin callers by default. That 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$includeSuperadminsisfalse, users in thesuperadmingroup are filtered out of results.countAll(bool $includeSuperadmins = false)- same filtering for counts.findById(int $id, bool $includeSuperadmins = true)- when$includeSuperadminsisfalse, returnsnullif the target user is in thesuperadmingroup. Defaults totruefor 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 happens post-fetch in PHP (not at the query level) due to ActiveRecord limitations with table-qualified column names in WHERE clauses. For paginated results, a page may contain fewer results than $perPage if superadmin users were in the batch.
Default seeds
The following seed data is inserted automatically whenever migrations run (enlivenapp/migrations applies package seed classes alongside migrations):
Groups: superadmin, admin, user
Permissions: admin.access, users.list, users.create, users.edit, users.delete, profile.edit
Group-permission mappings:
superadmin→*(all permissions)admin→admin.access,users.list,users.create,users.edit,users.deleteuser→profile.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 via the Authorizable trait (no ORM handle needed) $user->addGroup('editor'); $user->removeGroup('editor'); $user->addPermission('posts.create'); $user->addPermission('posts.create', true); // explicit deny $user->removePermission('posts.create'); // Or use the management services directly auth()->groups()->syncPermissions('editor', ['posts.create', 'posts.edit']); auth()->permissions()->create('posts.create', 'Create blog posts');
Note: typed-property assignments bypass ActiveRecord's change tracking, so Shield's trait methods use explicit dirty([...]) calls internally.
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, matching the NIST SP 800-63B minimum) and max length (128) |
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 isn't 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 show -n username 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
Every shield:user command that changes a user runs through the same Services\UserManagement logic used by the HTTP layer. So CLI calls validate identically: duplicate emails are rejected, and passwords must pass the configured validator pipeline (min length, personal-info checks, dictionary, and so on). show prints full detail for one user: groups, direct permissions including denies, last login, and status.
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 php runway shield:group syncpermissions -a editor -p "posts.create,posts.edit"
syncpermissions replaces the group's permission set with the given comma-separated list. Aliases that don't exist are reported and skipped; if none of the provided aliases resolve, the command aborts and the group is left unchanged.
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
Deleting a permission also cleans up everything referencing it: group-permission mappings (auth_group_permissions) and direct user grants/denies (auth_permissions_users) are removed in the same operation, so no orphaned rows remain.
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>
init, addkey, and removekey write to the host's app/config/shield.php override file. That file must already exist with an empty hmac block before you run them; otherwise the command reports that there is nothing to write to. The application merges this override file with the package defaults at load time.
Configuration
All Shield options live inside the plugin's config. The plugin merges your overrides with its defaults from src/Config/Config.php, so you only need to include values you want to override.
Provide your overrides in the per-app override config file at app/config/shield.php (resolved from PROJECT_ROOT). The application merges it over the package defaults at load time:
// app/config/shield.php - return the keys you want to override return [ 'routePrepend' => 'auth', 'default_authenticator' => 'session', // ... any other overrides ];
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 |
[] |
No-op in this port: NothingPersonalValidator pulls username/email from the identity record itself |
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_login_admin' => '/admin', '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 doesn't 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 runs for 2FA codes, magic links, and email activation messages.
Views
Shield renders its own views by default, shipping both .php and .tpl variants under src/Views/. To override any view, create a matching file under app/Views/<plugin-id>/. The plugin id is the package name, so login.php overrides to:
app/Views/enlivenapp/flight-shield/login.php
A theme can override it the same way. Replace <active> with the currently active theme's directory (for example themes/default/Views/enlivenapp/flight-shield/login.php).
Overrideable views:
login.phpregister.phpmagic_link_login.phpmagic_link_message.php2fa_verify.phpactivate.php
Security Notes
Passwords are hashed with password_hash() using PASSWORD_DEFAULT (bcrypt, cost 12). You can switch to Argon2 via algorithm, memory_cost, time_cost, and threads. Passwords rehash automatically when the cost parameters change.
Session storage is handled by enlivenapp/flight-sessions; see that package's README for how payloads are protected at rest.
HMAC secrets are stored encrypted with AES-256-GCM. Encryption keys live in app configuration (or better, injected from .env), never in the database.
All token and hash comparisons use hash_equals() to prevent timing attacks.
HMAC requests with a timestamp more than 300 seconds from time() are rejected, which blocks replay attempts.
Remember-me tokens use a split-token scheme (selector stored plain, validator stored as a SHA-256 hash). A mismatch purges all tokens for the user.
All mutating auth routes are protected by CsrfMiddleware from enlivenapp/flight-csrf.
Failed login attempts are tracked per IP across auth_logins and auth_token_logins. Excessive failures result in HTTP 429.
The session ID regenerates on login and logout, which prevents session fixation.
License
MIT - see LICENSE.