PHPackages                             enlivenapp/flight-shield - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. enlivenapp/flight-shield

ActiveFlightphp-plugin[Authentication &amp; Authorization](/categories/authentication)

enlivenapp/flight-shield
========================

Authentication and authorization for FlightPHP, ported from CodeIgniter Shield

0.2.1(2mo ago)01012MITPHPPHP &gt;=8.1

Since Apr 18Pushed 2mo agoCompare

[ Source](https://github.com/enlivenapp/FlightPHP-Shield)[ Packagist](https://packagist.org/packages/enlivenapp/flight-shield)[ RSS](/packages/enlivenapp-flight-shield/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (5)Versions (6)Used By (12)

[![Stable? Not Quite Yet](https://camo.githubusercontent.com/8712c441bb32fd8db507e0008c5d13e30def50064f8fcbc689fbcf5191f7d2af/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f737461626c652533462d6e6f7425323071756974652532307965742d626c75653f7374796c653d666f722d7468652d6261646765)](https://packagist.org/packages/enlivenapp/flight-shield)[![License](https://camo.githubusercontent.com/bb8c9663c981393dfeed20348e3110307d995d2a9ce83e63e9ea5554c7143748/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f656e6c6976656e6170702f666c696768742d736869656c643f7374796c653d666f722d7468652d6261646765)](https://packagist.org/packages/enlivenapp/flight-shield)[![PHP Version](https://camo.githubusercontent.com/994c9f317a105ce4d5974ad90304dc3605ec3792cd78bc8c54a44d2573893b28/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f656e6c6976656e6170702f666c696768742d736869656c643f7374796c653d666f722d7468652d6261646765)](https://packagist.org/packages/enlivenapp/flight-shield)[![Monthly Downloads](https://camo.githubusercontent.com/ddefc342a315a7769c135b2ed0f794fb8f75b1d6a3bb47fc769de3e797a6ecf0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646d2f656e6c6976656e6170702f666c696768742d736869656c643f7374796c653d666f722d7468652d6261646765)](https://packagist.org/packages/enlivenapp/flight-shield)[![Total Downloads](https://camo.githubusercontent.com/6fefc7c3871e8434f2fd38b8805bd8e45694793052a0561539788561d9b8aa81/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f656e6c6976656e6170702f666c696768742d736869656c643f7374796c653d666f722d7468652d6261646765)](https://packagist.org/packages/enlivenapp/flight-shield)[![GitHub Issues](https://camo.githubusercontent.com/c1e66524c1341cc27f0c61b43762d06d4d770f23aaad31615223d986127c02e6/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6973737565732f656e6c6976656e6170702f466c696768745048502d536869656c643f7374796c653d666f722d7468652d6261646765)](https://github.com/enlivenapp/FlightPHP-Shield/issues)[![Contributors](https://camo.githubusercontent.com/1267d80f4810c2354c087dd1fe05ee2153afefb7498cd5fc4c6e7208ab90cde9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732f656e6c6976656e6170702f466c696768745048502d536869656c643f7374796c653d666f722d7468652d6261646765)](https://github.com/enlivenapp/FlightPHP-Shield/graphs/contributors)[![Latest Release](https://camo.githubusercontent.com/efef4627d61077f90a11db74e718b12f026a95f120013fcfda8bd1eca5aaa5e8/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f656e6c6976656e6170702f466c696768745048502d536869656c643f7374796c653d666f722d7468652d6261646765)](https://github.com/enlivenapp/FlightPHP-Shield/releases)[![Contributions Welcome](https://camo.githubusercontent.com/9db3f8b82ea88469efd296a9305fc00f40b4c9450bb6b539d42d339fa6963444/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f6e747269627574696f6e732d77656c636f6d652d626c75653f7374796c653d666f722d7468652d6261646765)](https://github.com/enlivenapp/FlightPHP-Shield/pulls)

Flight Shield
=============

[](#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](https://flightphp.com/), ported and adapted from CodeIgniter Shield. It integrates into the [flight-school](https://github.com/enlivenapp/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
--------

[](#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
------------

[](#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
------------

[](#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_groups` — `superadmin`, `admin`, `user`
- `auth_permissions` — `admin.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
-----------

[](#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
------------

[](#how-it-works)

### Architecture overview

[](#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:

AliasClassTransport`session``Session`PHP session + optional remember-me cookie`tokens``AccessTokens``Authorization: Bearer ` header`hmac``HmacSha256``Authorization: HMAC-SHA256 :` + `X-Request-Timestamp` header`jwt``JWT``Authorization: Bearer ` headerJWT 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
----------------------

[](#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

[](#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)

[](#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

[](#configuration)

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

---

Access Token Authentication
---------------------------

[](#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
-------------------

[](#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 :
X-Request-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
------------------

[](#jwt-authentication)

The `JWT` authenticator is stateless. It reads `Authorization: Bearer `, 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,
],
```

3. 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
--------------------

[](#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
----------------------

[](#groups-and-permissions)

### Data model

[](#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

[](#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-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

[](#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)
- `admin` → `admin.access`, `users.list`, `users.create`, `users.edit`, `users.delete`
- `user` → `profile.edit`

### Working with groups and permissions in code

[](#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
-------------------

[](#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.

ValidatorWhat 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
-----------

[](#middlewares)

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

### `SessionAuthMiddleware`

[](#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`

[](#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`

[](#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`

[](#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`

[](#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`

[](#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`

[](#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`

[](#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`

[](#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
------------

[](#cli-commands)

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

### `shield`

[](#shield)

Displays available sub-commands.

```
php runway shield
```

---

### `shield:user`

[](#shielduser)

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`

[](#shieldgroup)

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`

[](#shieldpermission)

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`

[](#shieldhmac)

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

```

---

Configuration
-------------

[](#configuration-1)

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

[](#full-default-configuration)

KeyDefaultDescription`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-settings)

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

### Password settings

[](#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-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-settings)

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

### Rate limiting settings

[](#rate-limiting-settings)

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

### Redirect URLs

[](#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
------

[](#routes)

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

MethodPathNotesGET`/auth/login`Show login formPOST`/auth/login`Process login — CSRF + rate limitGET`/auth/logout`Log outGET`/auth/register`Show registration formPOST`/auth/register`Process registration — CSRFGET`/auth/magic-link`Show magic link request formPOST`/auth/magic-link`Send magic link email — CSRF + rate limitGET`/auth/magic-link/verify`Verify magic link tokenGET`/auth/2fa`Show 2FA verification page (sends code)POST`/auth/2fa/verify`Verify 2FA code — CSRF + rate limitPOST`/auth/2fa/resend`Resend 2FA code — CSRF + rate limitGET`/auth/activate`Show activation page (sends email)GET`/auth/activate/verify`Verify email activation token---

Email Setup
-----------

[](#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
-----

[](#views)

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

```
app/views/enlivenapp/flight-shield/.php

```

Overrideable views:

- `login.php`
- `register.php`
- `magic_link_login.php`
- `magic_link_message.php`
- `2fa_verify.php`
- `activate.php`

---

Security Notes
--------------

[](#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
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance84

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community15

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

5

Last Release

80d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3036663?v=4)[Mike W](/maintainers/enlivenapp)[@enlivenapp](https://github.com/enlivenapp)

---

Top Contributors

[![enlivenapp](https://avatars.githubusercontent.com/u/3036663?v=4)](https://github.com/enlivenapp "enlivenapp (5 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/enlivenapp-flight-shield/health.svg)

```
[![Health](https://phpackages.com/badges/enlivenapp-flight-shield/health.svg)](https://phpackages.com/packages/enlivenapp-flight-shield)
```

###  Alternatives

[vitalybaev/laravel5-dkim

Laravel 5/6 package for signing outgoing messages with DKIM.

3163.1k](/packages/vitalybaev-laravel5-dkim)[firemultimedia/mautic-multi-captcha-bundle

This plugin brings Google's reCAPTCHA, hCaptcha, and Cloudflare Turnstile integration to mautic.

141.3k](/packages/firemultimedia-mautic-multi-captcha-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
