# HTTP Server CLI — Authentication

Bootgly HTTP authentication is split into small layers:

- **Request credentials** parse Basic `Authorization` headers without trusting them.
- **Request token metadata** exposes Bearer transport as `$Request->token`.
- **Authentication guards** verify credentials and emit protocol-aware challenges.
- **The `Authentication` middleware** runs one or more guards around protected routes.

Supported mechanisms in HTTP Server CLI are:

| Mechanism | Use case | Transport |
| --- | --- | --- |
| Basic | Compatibility, development, simple protected endpoints | `Authorization: Basic ...` |
| Bearer | Opaque API tokens and access tokens | `Authorization: Bearer <token>` |
| JWT | Signed compact tokens verified by Bootgly | `Authorization: Bearer <jwt>` |
| Session | Browser flows backed by `$Request->Session` | Session cookie |

> Digest authentication is intentionally not part of this first implementation.

## Request credentials

`$Request->authenticate()` parses Basic HTTP `Authorization` credentials and returns a credentials object or `null`:

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request\Authentications\Basic;

$Credentials = $Request->authenticate();

if ($Credentials instanceof Basic) {
   $username = $Credentials->username;
   $password = $Credentials->password;
}
```

The parser does not verify credentials. Verification belongs to guards and application resolvers. Bearer and JWT tokens stay inside `Router\Middlewares`: read `$Request->token` or use the Bearer/JWT guards.

The request also exposes lazy authentication metadata:

```php
$Request->username; // Basic username
$Request->password; // Basic password
$Request->token;    // Bearer token
```

## Response challenges

Use `$Response->authenticate()` to return a Basic `401 Unauthorized` challenge. Bearer challenges are emitted by the Bearer/JWT guards so their definitions stay inside `Router\Middlewares`.

### Basic challenge

```php
use Bootgly\WPI\Modules\HTTP\Server\Response\Authentication\Basic;

return $Response->authenticate(new Basic(
   realm: 'Bootgly Protected Area'
));
```

Emits:

```http
WWW-Authenticate: Basic realm="Bootgly Protected Area"
```

### Bearer challenge

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication\Bearer;

$Bearer = new Bearer(
   Resolver: fn (string $token): bool => $token === 'demo-bearer-token',
   realm: 'Bootgly API',
   error: 'invalid_token',
   description: 'The access token is missing or invalid.',
   URI: 'https://docs.bootgly.com/manual/WPI/HTTP/HTTP_Server_CLI/Authentication',
   scope: 'demo:read'
);
```

On failure, the guard emits a Bearer `WWW-Authenticate` header with RFC 6750-style attributes.

## Authentication middleware

The middleware receives an `Authenticating` strategy object. The strategy stores guards in evaluation order.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authenticating;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication;
```

When any guard authenticates successfully, the route handler runs. When all guards fail, the middleware uses the first guard to build the challenge response.

`Authentication` requires at least one guard. Creating it with an empty `Authenticating` strategy throws `InvalidArgumentException` so protected routes cannot fail closed silently because of misconfiguration.

Custom fallback callbacks may render a body or extra headers for denied requests, but the middleware normalizes the returned response to `401 Unauthorized` before and after the callback. This prevents accidental `200 OK` responses on authentication failure. Redirecting fallbacks are the one exception: a callback result that is already a redirect (3xx status + `Location` header) is returned untouched, so browser/session flows can send guests to a sign-in page with a real `303`.

```php
$Auth = new Authentication(
   Authenticating: $Bearer,
   Fallback: function (Request $Request, Response $Response): Response {
      return $Response(body: 'Custom unauthorized body');
   }
);
```

Authentication guards expose metadata through declared `Request` properties: `$Request->identity` for the authenticated principal and `$Request->claims` for verified token claims. Request doubles should declare those properties too, or use `stdClass` in lightweight tests.

## Hardening notes

- Pair Basic and Bearer routes with `RateLimit` to reduce brute-force attempts.
- Resolver callbacks should compare secrets with `hash_equals()` when checking passwords, API tokens, or shared secrets.
- Place CSRF protection before state-changing authenticated browser routes; token/API routes that do not use cookies can be exempted by route policy.
- JWT is transported as a Bearer token and shares the `$Request->token` hook with opaque Bearer credentials.

## Bearer token guard

Use the Bearer guard for opaque API tokens. The resolver receives the token and the `Request`.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Response;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authenticating;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication\Bearer;

$Bearer = new Authenticating(
   new Bearer(function (string $token, Request $Request): bool {
      return $token === 'demo-bearer-token';
   })
);

yield $Router->route('/auth/bearer', function (Request $Request, Response $Response) {
   return $Response->JSON->send([
      'authorized' => true,
      'guard' => 'Bearer',
   ]);
}, GET, middlewares: [new Authentication($Bearer)]);
```

Try it:

```bash :toolbar="true";
curl -H 'Authorization: Bearer demo-bearer-token' http://localhost:8082/auth/bearer
```

A resolver may return:

- `false` or `null` to deny.
- `true` to expose the token as `$Request->identity`.
- any custom value to expose that value as `$Request->identity`.

## JWT guard

Bootgly includes a native JWT signer/verifier in `Bootgly\API\Security\JWT`. JWT is not a separate HTTP scheme; it uses Bearer transport. Create the JWT object once at application boot and share it across requests instead of rebuilding the key set per request.

```php
use Bootgly\API\Security\JWT;
use Bootgly\API\Security\JWT\Policies;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Response;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authenticating;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication\JWT as JWTGuard;

$Token = new JWT('bootgly-demo-authentication-secret');

yield $Router->route('/auth/jwt/issue', function (Request $Request, Response $Response) use ($Token) {
   $token = $Token->sign([
      'sub' => 'demo-user',
      'scope' => 'demo:read',
      'exp' => time() + 3600,
   ]);

   return $Response->JSON->send([
      'token' => $token,
      'authorization' => "Bearer {$token}",
   ]);
}, GET);

$Policies = new Policies(
   issuers: 'https://issuer.bootgly.dev',
   audiences: 'api://bootgly-demo',
   subject: true
);
$JWT = new Authenticating(new JWTGuard($Token, $Policies));

yield $Router->route('/auth/jwt', function (Request $Request, Response $Response) {
   return $Response->JSON->send([
      'authorized' => true,
      'guard' => 'JWT',
   ]);
}, GET, middlewares: [new Authentication($JWT)]);
```

JWT signing throws `RuntimeException` when claims or headers cannot be JSON encoded. JWT verification rejects malformed tokens, unsupported algorithms, unsupported `typ` values, invalid signatures, expired `exp`, future `nbf`, and future `iat`. The HS256 secret must be at least 32 bytes. `Policies` can require exact `iss`, matching `aud`, non-empty `sub`, and non-empty `jti` claims. The guard still returns only a generic Bearer `invalid_token` challenge to the client.

The default `JWT->leeway` is `0`, so time claim verification is strict. Set a small value such as `5` seconds when your servers can have minor clock drift.

For deterministic tests, use `JWT->freeze($timestamp)` and then `JWT->resume()` to return to the wall clock.

When the `sub` claim is present, the guard exposes `Bootgly\API\Security\Identity` as `$Request->identity`. It always exposes verified claims as `$Request->claims` and verified protected headers as `$Request->tokenHeaders`. A space-separated JWT `scope` claim or an array/string `scp` claim is normalized into `Identity->scopes`, so `$Request->identity->check('demo:read')` works for JWT users. When both are present, `scope` takes precedence over `scp`.

### RS256, JWKS, and key rotation

Use `Bootgly\API\Security\JWT\Key` for explicit key ids and `Bootgly\API\Security\JWT\KeysJWKS` for local JWKS documents:

```php
use Bootgly\API\Security\JWT;
use Bootgly\API\Security\JWT\Key;
use Bootgly\API\Security\JWT\KeysJWKS;

$Signer = new JWT($privatePem, 'RS256');
$Signer->select(new Key($privatePem, 'RS256', 'current'));

$Verifier = new JWT($publicPem, 'RS256');
$Verifier->trust(KeysJWKS::parse($jwks, 'RS256'));
```

Always set `kid` when rotating JWT keys. A no-`kid` key set is intentionally single-slot for backwards-compatible tokens; adding a second default key fails loudly instead of letting the verifier guess.

For external OAuth/OIDC issuers, use `Bootgly\API\Security\JWT\Remote` with the provider's `jwks_uri`:

```php
use Bootgly\API\Security\JWT;
use Bootgly\API\Security\JWT\Policies;
use Bootgly\API\Security\JWT\Remote;
use Bootgly\API\Security\JWT\Vault;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authenticating;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication\JWT as JWTGuard;

$Remote = new Remote('https://issuer.example/.well-known/jwks.json');
$Remote->TTL = 900; // public property is uppercase; constructor argument remains ttl:
$Remote->cache(new Vault);
$Verifier = new JWT($Remote, 'RS256');

$Policies = new Policies(
   issuers: 'https://issuer.example',
   audiences: 'api://bootgly-demo',
   subject: true
);

$JWT = new Authenticating(new JWTGuard($Verifier, $Policies));
```

`Remote` keeps a process-local `KeySet` and can use `Vault` to share versioned JWKS records across workers. The mutable public `$TTL` property (uppercase) is the operator ceiling, from `0` through `31,536,000` seconds; the constructor's named argument remains `ttl:`. Construction and later `$TTL` assignments reject negative or larger values without replacing the last valid setting. The former lowercase `$ttl` property is no longer public.

Redirects are followed one hop at a time, and every target must remain HTTPS unless `insecure: true` was explicitly selected for tests or a controlled local environment. `Location` URI references are resolved according to RFC 3986: dot segments are removed without collapsing meaningful consecutive slashes, and fragments are never sent in the request target. Only the final response supplies the accepted status and cache headers. Its effective lifetime is the shortest `Cache-Control: max-age`, reduced by `Age` and capped by `$TTL`; `no-store`, `no-cache`, and `max-age=0` make the response immediately stale and prevent a shared-cache write. Shared readers inherit the writer's absolute expiry instead of starting a new TTL. `Remote` refreshes on an unknown `kid` and fails closed for fetch errors, invalid redirect targets, final non-2xx responses, invalid JSON, or invalid JWKS. OIDC Discovery, `ETag`/`Last-Modified`, and exponential backoff remain future layers.

`Vault` stores its records on the Bootgly `Cache` facade. By default it uses the `file` driver (shared across workers on the same filesystem); passing a Redis-backed `Cache` shares JWKS, refresh-token state, and revocations across hosts — inject a shared HMAC secret (≥ 32 bytes) so every host can verify the records:

```php
use Bootgly\ABI\Resources\Cache;
use Bootgly\API\Security\JWT\Vault;

$Vault = new Vault(
   new Cache(['driver' => 'redis', 'host' => 'redis.internal']),
   secret: getenv('JWT_VAULT_SECRET')
);
```

Every record carries an HMAC-SHA256 envelope, so an entry altered in the storage backend fails verification and reads as a miss.

### Refresh tokens and `jti` usage

For first-party fullstack apps, keep access tokens short-lived and rotate opaque refresh tokens through `Bootgly\API\Security\JWT\Tokens`:

```php
use Bootgly\API\Security\JWT\Replay;
use Bootgly\API\Security\JWT\Token;
use Bootgly\API\Security\JWT\Tokens;
use Bootgly\API\Security\JWT\Usage;
use Bootgly\API\Security\JWT\Vault;

$Vault = new Vault;
$Tokens = new Tokens($Vault);

$Issued = $Tokens->mint('user-42', 60 * 60 * 24 * 30, [
   'role' => 'admin',
]);

$Rotated = $Tokens->rotate($Issued->refresh, 60 * 60 * 24 * 30);
if ($Rotated instanceof Replay) {
   // incident: log subject/family and force logout for sibling sessions
}
elseif ($Rotated instanceof Token) {
   // issue the new refresh token to the client
}
```

`Tokens` stores refresh state under token hashes, consumes the old refresh token on rotation, leaves a tombstone with `subject`/claims for audit, and returns `Replay` if a consumed refresh token is reused. Replay revokes the whole family and should be treated as an incident.

Use `Usage` when access-token `jti` values need persistent revocation or single-use replay protection:

```php
$Usage = new Usage($Vault);
$Verifier->track($Usage);

$Usage->block('access-token-jti', 300);

$SingleUse = new Usage($Vault, single: true);
$Verifier->track($SingleUse);

$OptionalJTI = new Usage($Vault, required: false);
```

`Usage` runs only after signature, temporal claim validation, and optional `Policies` pass. By default it requires the `jti` claim; use `required: false` only when tokens without an identifier should pass without persistent revocation. Single-use mode requires an `exp` claim so the seen `jti` marker expires automatically.

## Basic guard

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Response;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authenticating;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication\Basic as BasicGuard;

$Basic = new Authenticating(
   new BasicGuard(function (string $username, string $password, Request $Request): bool {
      return $username === 'demo' && $password === 'secret';
   })
);

yield $Router->route('/auth/basic', function (Request $Request, Response $Response) {
   return $Response->JSON->send([
      'authorized' => true,
      'guard' => 'Basic',
   ]);
}, GET, middlewares: [new Authentication($Basic)]);
```

Try it:

```bash :toolbar="true";
curl -u demo:secret http://localhost:8082/auth/basic
```

## Session guard

Use the Session guard when authentication state lives in `$Request->Session`. It checks a session key and exposes the stored value as `$Request->identity`.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authenticating;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication\Session as SessionGuard;

$Session = new Authenticating(new SessionGuard(key: 'identity'));

yield $Router->route('/account', function ($Request, $Response) {
   return $Response->JSON->send([
      'authorized' => true,
   ]);
}, GET, middlewares: [new Authentication($Session)]);
```

Session failures return a generic `401 Unauthorized` response without a `WWW-Authenticate` header.

## Remember guard

Use the Remember guard to revive sessions from a persistent trusted-device cookie (remember-me). It validates and rotates the token through the `Bootgly\API\Security\Tokens\Trust` store, regenerates the session id (fixation defense), installs the identity in the session and re-emits the rotated cookie.

```php
use Bootgly\API\Security\Tokens\Trust;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authenticating;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication\Remember;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication\Session as SessionGuard;

$Trust = new Trust($Response->Database->Database);
$Auth = new Authentication(new Authenticating(
   new SessionGuard,          // cheap session check wins
   new Remember($Trust)       // cookie revival only on session miss
));
```

The guard owns the remember cookie: login flows call `emit()` after `Trust->issue()` and logout flows call `forget()`. The cookie policy is framework-owned through statics (`Remember::$name`, `$lifetime`, `$secure`, `$httpOnly`, `$sameSite`) — hardened defaults that php.ini cannot downgrade.

`Trust` keeps only the immediately previous validator digest for a fixed,
private five-second grace window. When an in-flight duplicate presents that
exact validator during the window, the guard declines without authenticating,
clearing the cookie or revoking devices. A replay after the window, or any
unrelated wrong validator for a known series, is the stolen-cookie signature:
the store reports `Theft` and the guard clears the cookie only after every
device has been successfully revoked. If that revocation fails, `rotate()`
returns `null` instead of fabricating a theft incident.

`Tokens->revoke()` and `Trust->revoke()` return `null|int`: `0` means the
statement succeeded without matching a row, while `null` means a recorded
database failure. Callers that promise logout-everywhere or credential
invalidation must treat `null` as an infrastructure failure.

The action-token `tokens` table must enforce `UNIQUE (user_id, purpose)`.
`mint()` uses one atomic upsert, so two concurrent callers can both receive a
token while only the winning value remains valid. Apply the Auth scaffold's
`20260822000200_unique_tokens_user_purpose` migration before deploying this
code. Pause action-token writers while MySQL performs the non-transactional
deduplication/index transition. For rollback, restore and drain the old code
first, then optionally remove the unique index after no new worker remains.

The underlying `trusts` table must provide nullable `previous VARCHAR(64)` and
`rotated BIGINT` columns; apply both additive migrations before deploying and
restart or drain all workers as one cohort.

Credential, action-token and trusted-device verdicts are always read from the
primary database, even when replicas are configured. Transaction-backed stores
use `SELECT ... FOR UPDATE` on MySQL/PostgreSQL; PostgreSQL Repeatable Read can
fail closed with SQLSTATE `40001` and requires rollback plus a whole-transaction
retry. SQLite uses a zero-row writer barrier and can fail closed with
`database is locked` for a stale snapshot. Locks last until commit/rollback,
read-only transactions fail closed, and these transactions should remain
short. Constructor signatures do not change; revocation return types are
nullable as described above. See the
**[Authentication guide](/guide/authentication/overview/)** for the complete
session/cookie scaffold and upgrade sequence.

## Multiple guards

You can compose guards in order:

```php
$API = new Authenticating(new JWTGuard($Token));
$API->add(new Bearer(function (string $token, Request $Request): bool {
   return $token === 'legacy-api-token';
}));

yield $Router->route('/api/private', $Handler, GET, middlewares: [new Authentication($API)]);
```

In this example Bootgly tries JWT first, then the opaque Bearer token. If both fail, the JWT guard defines the challenge because it is the first guard.

## Demo project

The repository includes working examples in `projects/Demo/HTTP_Server_CLI`:

- `router/routes/Authentication.routes.php`

Add `'Authentication'` to `router/router.index.php`, start the demo server, then open `GET /auth` to see runnable commands for Bearer, JWT, and Basic routes.

For the session/cookie flows (registration, e-mail verification, login + remember-me, password reset), see the exportable **Auth** demo in `bootgly-web/projects/Auth` and the **[Authentication guide](/guide/authentication/overview/)**.
