Skip to content

How-to: Bearer Token Middleware (JWT Auth Edge Cases)

FT reference: FT273 (NENE2-FT/authlog) — BearerTokenMiddleware JWT auth: alg=none rejection, signature tampering detection, exp/nbf enforcement, WWW-Authenticate header, per-sub data isolation, IDOR → 404, 18 tests / 26 assertions PASS.

VULN assessment: V-01 through V-10 included at the end of this document.

Demonstrates using NENE2's BearerTokenMiddleware + LocalBearerTokenVerifier (HMAC-HS256) to protect routes. All JWT validation edge cases are handled by the middleware; controllers only receive decoded claims via nene2.auth.claims.


Setup

php
$verifier        = new LocalBearerTokenVerifier($secret); // env: NENE2_LOCAL_JWT_SECRET
$bearerMiddleware = new BearerTokenMiddleware($problems, $verifier);

$app = (new RuntimeApplicationFactory(
    $psr17, $psr17,
    routeRegistrars: [static fn (Router $r) => $registrar->register($r)],
    authMiddleware:  $bearerMiddleware,
))->create();

The middleware sets nene2.auth.claims on the request before any route handler runs. If validation fails, it returns 401 with WWW-Authenticate: Bearer before the handler is invoked.


Extracting claims in a controller

php
private function resolveOwnerId(ServerRequestInterface $request): string
{
    /** @var array<string, mixed> $claims */
    $claims = $request->getAttribute('nene2.auth.claims') ?? [];
    return (string) ($claims['sub'] ?? '');
}

The sub claim is the canonical user identity. Using it as owner_id ensures per-user data isolation without any additional lookup.


WWW-Authenticate header

On 401, the middleware emits WWW-Authenticate: Bearer realm="api". On expired tokens, the header includes error="invalid_token":

WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="..."

RFC 6750 compliance allows clients to distinguish "no token" from "bad token."


Vulnerability Assessment

V-01 — alg=none algorithm substitution ✅ SAFE

Risk: An attacker crafts a JWT with "alg":"none" and an unsigned payload claiming sub: admin. Finding: SAFE — LocalBearerTokenVerifier only accepts HMAC-HS256. alg=none tokens are rejected at signature verification; the test testWrongAlgorithmHeaderReturns401 confirms 401.


V-02 — Signature tampering ✅ SAFE

Risk: Attacker intercepts a valid JWT and modifies the payload (e.g., changes sub to admin) while keeping the header and original signature. Finding: SAFE — HMAC-HS256 signature covers header.payload. Any modification invalidates the MAC; testTamperedPayloadReturns401 confirms 401.


V-03 — Expired token replay ✅ SAFE

Risk: An expired token is replayed after the session should be invalid. Finding: SAFE — exp claim is validated; tokens with exp < time() are rejected. testExpiredTokenReturns401 confirms 401 with invalid_token in WWW-Authenticate.


V-04 — Not-before (nbf) bypass ✅ SAFE

Risk: A token with a future nbf (not valid yet) is used before its activation time. Finding: SAFE — nbf is enforced; testNbfInFutureReturns401 confirms 401.


V-05 — Wrong Authorization scheme ✅ SAFE

Risk: Attacker sends Authorization: Basic dXNlcjpwYXNz or omits the Bearer prefix. Finding: SAFE — the middleware only accepts tokens prefixed with Bearer . Basic and bare token strings both return 401.


V-06 — Malformed token structure ✅ SAFE

Risk: Attacker sends tokens with 2 parts, 4 parts, non-base64 payload, or random strings to probe error handling. Finding: SAFE — all malformed variants return 401. Non-3-part tokens and invalid base64 are rejected before any claim extraction.


V-07 — Wrong signing secret ✅ SAFE

Risk: An attacker with knowledge of the JWT format signs a token with a different secret. Finding: SAFE — HMAC verification fails if the secret differs; testWrongSecretSignatureReturns401 confirms 401.


V-08 — IDOR: cross-user data access ✅ SAFE

Risk: User A attempts to read User B's data by knowing or guessing the entry ID. Finding: SAFE — findByIdAndOwner($id, $ownerId) scopes the lookup to the JWT sub. A cross-user request returns 404 (not 403) to avoid revealing that the entry exists.


V-09 — Per-user data isolation ✅ SAFE

Risk: User A's writes are visible to User B. Finding: SAFE — all reads are scoped by owner_id = sub. testEntriesAreIsolatedByToken verifies that Alice's and Bob's entries are fully separated.


V-10 — Token without exp claim ✅ SAFE (acceptable)

Risk: A token with no exp claim is issued, becoming effectively non-expiring. Finding: SAFE (by design) — LocalBearerTokenVerifier only validates exp if the claim is present. Tokens without exp are accepted. This is a deliberate trade-off for service-to-service scenarios; production deployments should enforce exp via a stricter verifier if needed.


VULN Summary

IDVulnerabilityFinding
V-01alg=none algorithm substitution✅ SAFE
V-02Signature tampering✅ SAFE
V-03Expired token replay✅ SAFE
V-04Not-before (nbf) bypass✅ SAFE
V-05Wrong Authorization scheme✅ SAFE
V-06Malformed token structure✅ SAFE
V-07Wrong signing secret✅ SAFE
V-08IDOR cross-user data access✅ SAFE
V-09Per-user data isolation✅ SAFE
V-10Token without exp claim✅ SAFE (by design)

10 SAFE, 0 EXPOSED No critical vulnerabilities. The BearerTokenMiddleware handles all standard JWT attack vectors; application code only needs to use the sub claim for ownership scoping.


What NOT to do

Anti-patternRisk
Accept alg=none tokensAttacker can forge any identity by omitting the signature
Skip exp validationStolen tokens remain valid indefinitely
Return 403 on IDORReveals that the resource exists and belongs to someone else
Use X-User-Id header instead of JWT subHeader is trivially spoofable; JWT claim is cryptographically bound
Share the signing secret across environmentsA dev-env leak compromises production tokens
Use RS256 keys smaller than 2048 bitsVulnerable to factoring attacks

Released under the MIT License.