$npx -y skills add Lonsdale201/wp-agent-skills --skill br-jwks-jwt-authConfigure Better Route 1.1 RS256 or ES256 JWT verification from a local or HTTPS JWKS. Use when integrating OIDC/OAuth bearer tokens, selecting keys by kid, validating issuer/audience/lifetime, or operating JWKS caching and refresh behavior safely.
| 1 | # Better Route JWKS JWT authentication |
| 2 | |
| 3 | Use `Rs256JwksJwtVerifier` for asymmetric bearer JWTs and adapt it to the generic bearer middleware. |
| 4 | |
| 5 | ```php |
| 6 | use BetterRoute\Middleware\Auth\BearerTokenAuthMiddleware; |
| 7 | use BetterRoute\Middleware\Auth\JwtBearerTokenVerifierAdapter; |
| 8 | use BetterRoute\Middleware\Jwt\HttpJwksProvider; |
| 9 | use BetterRoute\Middleware\Jwt\Rs256JwksJwtVerifier; |
| 10 | |
| 11 | $jwks = new HttpJwksProvider( |
| 12 | jwksUri: 'https://issuer.example/.well-known/jwks.json', |
| 13 | ttlSeconds: 3600, |
| 14 | issuer: 'https://issuer.example', |
| 15 | minimumRefreshIntervalSeconds: 30 |
| 16 | ); |
| 17 | |
| 18 | $verifier = new Rs256JwksJwtVerifier( |
| 19 | jwks: $jwks, |
| 20 | leewaySeconds: 60, |
| 21 | expectedIssuer: 'https://issuer.example', |
| 22 | expectedAudience: 'my-api', |
| 23 | requireExpiration: true, |
| 24 | maxLifetimeSeconds: 3600, |
| 25 | allowedAlgorithms: ['RS256'], |
| 26 | kidMissRefreshCooldownSeconds: 30 |
| 27 | ); |
| 28 | |
| 29 | $auth = new BearerTokenAuthMiddleware( |
| 30 | verifier: new JwtBearerTokenVerifierAdapter($verifier), |
| 31 | requiredScopes: ['orders:read'] |
| 32 | ); |
| 33 | |
| 34 | $router->get('/orders', $handler) |
| 35 | ->middleware([$auth]) |
| 36 | ->protectedByMiddleware('bearerAuth'); |
| 37 | ``` |
| 38 | |
| 39 | ## Verification contract |
| 40 | |
| 41 | - Require a non-empty JOSE `alg` and `kid`. |
| 42 | - Allow only explicitly configured `RS256` and/or `ES256`; `none`, `HS*`, and other algorithms are rejected. |
| 43 | - Match `kid` to exactly one usable signing key. Ambiguous, incompatible, or absent matches fail closed. |
| 44 | - Require `exp` by default. Setting `maxLifetimeSeconds` also requires `iat` and bounds `exp - iat`. |
| 45 | - Pin both `expectedIssuer` and `expectedAudience` for production integrations. |
| 46 | - Keep token size, clock leeway, and key-refresh cooldown bounded. |
| 47 | |
| 48 | ## Remote JWKS behavior |
| 49 | |
| 50 | `HttpJwksProvider` accepts HTTPS URLs only and rejects URL credentials. Its WordPress transport uses `wp_safe_remote_get()`, TLS verification, a ten-second timeout, at most one redirect, and a 256 KiB response limit. |
| 51 | |
| 52 | It caches sanitized public keys in memory and a transient. Refreshes use a persistent cooldown and, when `$wpdb` is available, a bounded MySQL named lock. A failed refresh preserves the last known-good cached key set. A `kid` miss can trigger a refresh, but the verifier has its own cooldown to prevent attacker-driven fetch storms. |
| 53 | |
| 54 | The `better_route/jwks_refresh` action clears matching caches. Supply the provider `issuer` so a targeted action does not flush unrelated providers. `StaticJwksProvider` is appropriate for pinned or test keys. |
| 55 | |
| 56 | ## Operational checks |
| 57 | |
| 58 | - Confirm WordPress HTTP SSRF protection is not bypassed with a custom `httpGet` callback. |
| 59 | - Exercise signing-key rotation: cached old key, new `kid`, refresh, then successful verification. |
| 60 | - Exercise refresh failure and verify the last known-good set remains usable. |
| 61 | - Test duplicate `kid`, wrong `kty`/`crv`, mismatched key `alg`/`use`, invalid signature, and stale token. |
| 62 | - Never fetch a JWKS URL selected by an untrusted request. |
| 63 | |
| 64 | Source references: `src/Middleware/Jwt/HttpJwksProvider.php`, `src/Middleware/Jwt/Rs256JwksJwtVerifier.php`, `src/Middleware/Jwt/JwksKeySanitizer.php`, `src/Middleware/Auth/JwtBearerTokenVerifierAdapter.php`. |
| 65 | |
| 66 | ## References |
| 67 | |
| 68 | - Official documentation: <https://lonsdale201.github.io/better-docs/docs/better-route/agents> |