JWT and Bearer
better-route ships two JWT verifiers:
Hs256JwtVerifier— symmetric (shared secret), since 0.3.0.Rs256JwksJwtVerifier— asymmetric (RS256/ES256) backed by JWKS, since 0.6.0.
Both implement JwtVerifierInterface and plug into JwtAuthMiddleware the same way. Pick HS256 for first-party tokens you sign yourself; pick the JWKS verifier for OIDC providers that publish a key set.
JWT with HS256 verifier
use BetterRoute\Middleware\Jwt\Hs256JwtVerifier;
use BetterRoute\Middleware\Jwt\JwtAuthMiddleware;
use BetterRoute\Middleware\Auth\WpClaimsUserMapper;
$verifier = new Hs256JwtVerifier(
secret: $_ENV['JWT_SECRET'],
leewaySeconds: 30,
expectedIssuer: 'https://issuer.example.com',
expectedAudience: 'myapp',
requireExpiration: true, // v0.3.0 default — set false to allow tokens without `exp`
maxLifetimeSeconds: 3600, // optional cap on `exp - iat`
maxTokenLength: 8192 // v0.3.0 default
);
$jwtMiddleware = new JwtAuthMiddleware(
verifier: $verifier,
requiredScopes: ['content:*'],
userMapper: new WpClaimsUserMapper()
);
v0.6.0 internals
Hs256JwtVerifier was rewired to use the shared Crypto helper for constant-time signature comparison and base64url decoding. Public behavior is unchanged — the same shared crypto primitives are now reusable outside the verifier.
v0.3.0 hardening
Hs256JwtVerifier enforces (defaults in parentheses):
exprequired by default (requireExpiration: true). Tokens withoutexpare rejected unless explicitly disabled.expectedIssuer/expectedAudiencefor strictiss/audvalidation when set.audmatches against either a string or a list of audiences in the token.maxLifetimeSecondsrejects tokens whoseexp - iatexceeds the cap.maxTokenLength(8192bytes) rejects oversized tokens before parsing.
WpClaimsUserMapper defaults changed: idClaims is now ['user_id', 'uid', 'wp_user_id']. Re-add 'sub' explicitly if your tokens use it as the WP user identifier.
JWT with RS256/ES256 (JWKS) verifier
For tokens issued by an OIDC provider, use Rs256JwksJwtVerifier with HttpJwksProvider:
use BetterRoute\Middleware\Jwt\HttpJwksProvider;
use BetterRoute\Middleware\Jwt\Rs256JwksJwtVerifier;
$jwks = new HttpJwksProvider(
jwksUri: 'https://issuer.example.com/.well-known/jwks.json',
issuer: 'https://issuer.example.com'
);
$verifier = new Rs256JwksJwtVerifier(
jwks: $jwks,
expectedIssuer: 'https://issuer.example.com',
expectedAudience: 'better-route',
allowedAlgorithms: ['RS256']
);
The full hardening rules (strict kid matching, algorithm pinning, private-field stripping) live on the dedicated JWKS RS256 / ES256 page.
Bearer middleware with custom verifier
use BetterRoute\Middleware\Auth\BearerTokenAuthMiddleware;
$bearer = new BearerTokenAuthMiddleware(
verifier: $customVerifier,
requiredScopes: ['api:read'],
userMapper: new WpClaimsUserMapper(),
provider: 'partner_bearer'
);
Scope matching
Wildcard matching is supported both ways:
- required
content:*matches grantedcontent:read - required
content:readmatches grantedcontent:*
BearerTokenAuthMiddleware and JwtAuthMiddleware accept both array scope claims and OIDC-style space-delimited strings. 0.6.0 adds regression coverage for the string-shape behavior — no contract change.
Common mistakes
- Empty JWT secret
- Using a non-HS256 token with
Hs256JwtVerifier— switch toRs256JwksJwtVerifierfor RS256/ES256 - Expecting scopes from an unsupported claim shape
- (v0.3.0) issuing tokens without
expwhilerequireExpirationis on - (v0.3.0) relying on
subfor WP user mapping without re-adding it toWpClaimsUserMapper::$idClaims
Validation checklist
- malformed token ->
401 invalid_token - scope mismatch ->
403 insufficient_scope - mapped WP user is set when mapper resolves userId