AgentStack
SKILL verified MIT Self-run

Br Auth Middleware

skill-lonsdale201-wp-agent-skills-br-auth-middleware · by Lonsdale201

Pick and configure the right authentication middleware for

No reviews yet
0 installs
6 views
0.0% view→install

Install

$ agentstack add skill-lonsdale201-wp-agent-skills-br-auth-middleware

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Br Auth Middleware? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

better-route: Authentication middleware

For developers protecting endpoints with authentication via better-route's middleware pipeline. Core patterns: JWT, custom bearer tokens, WordPress application passwords, and browser cookie + nonce auth. Since 0.6.0, JWT verification can be HS256 or asymmetric RS256/ES256 from JWKS.

Misconception this skill corrects

> "I'll use JwtAuthMiddleware with my existing JWT tokens — they don't have an exp claim because they're long-lived."

Post-v0.3.0, Hs256JwtVerifier REQUIRES an exp claim by default ([src/Middleware/Jwt/Hs256JwtVerifier.php:23](Hs256JwtVerifier.php) — requireExpiration = true). Tokens without exp are rejected at line 116. The fix:

// EITHER add exp to your tokens (preferred — long-lived no-exp tokens are a security smell):
$verifier = new Hs256JwtVerifier(secret: $secret);  // exp required, default

// OR explicitly disable for migration scenarios:
$verifier = new Hs256JwtVerifier(secret: $secret, requireExpiration: false);

The same release also removed sub from WpClaimsUserMapper's default idClaims ([WpClaimsUserMapper.php:30](WpClaimsUserMapper.php)):

array $idClaims = ['user_id', 'uid', 'wp_user_id'],

If your tokens use sub as the user identifier, re-add it explicitly:

$mapper = new WpClaimsUserMapper(idClaims: ['sub', 'user_id', 'uid', 'wp_user_id']);

Other AI-prone misconceptions:

  • "I'll combine JwtAuthMiddleware with ->permission(...) for belt-and-suspenders." Wrong — pick one. Use ->protectedByMiddleware('jwt') to defer to the middleware OR ->permission(callable) for WP-cap checks. Combining means BOTH must pass; usually overshoots intent.
  • "I'll bind the JWT middleware to a specific route via ->middleware(...) and forget about route-layer permission." Wrong (post-v0.4.0) — ->middleware(...) adds the JWT to the pipeline, but the WP permission layer still denies writes by default. You also need ->protectedByMiddleware() to set WP permission to __return_true.
  • "CookieNonceAuthMiddleware works for AJAX from logged-in browser users automatically." Almost — needs the request to carry an X-WP-Nonce header (or a body field) AND the user to be logged in. AJAX from logged-in users naturally carries WP cookies; you need to manually attach the nonce client-side via wpApiSettings.nonce.

When to use this skill

Trigger when ANY of the following is true:

  • The diff instantiates JwtAuthMiddleware, BearerTokenAuthMiddleware, ApplicationPasswordAuthMiddleware, or CookieNonceAuthMiddleware.
  • The diff calls Hs256JwtVerifier, WpClaimsUserMapper.
  • A route uses ->protectedByMiddleware(...).
  • Tokens that worked pre-v0.3.0 now fail with invalid_token.

Workflow

1. Choose the right middleware

| Use case | Middleware | |---|---| | Server-issued JWT tokens (HS256, you control the secret) | JwtAuthMiddleware + Hs256JwtVerifier | | OIDC/OAuth bearer JWTs (RS256/ES256 from JWKS) | BearerTokenAuthMiddleware + JwtBearerTokenVerifierAdapter + Rs256JwksJwtVerifier | | Custom bearer tokens (opaque tokens you verify against your DB / third-party) | BearerTokenAuthMiddleware + your BearerTokenVerifierInterface | | Native WP authentication (admin tools, mobile apps using WP user accounts) | ApplicationPasswordAuthMiddleware | | Browser-side AJAX from logged-in users | CookieNonceAuthMiddleware |

2. JWT auth (most common for SPA / mobile clients)

use \BetterRoute\Middleware\Jwt\JwtAuthMiddleware;
use \BetterRoute\Middleware\Jwt\Hs256JwtVerifier;
use \BetterRoute\Middleware\Auth\WpClaimsUserMapper;

$verifier = new Hs256JwtVerifier(
    secret: 'your-secret-key',          // shared secret with the issuer
    leewaySeconds: 30,                  // clock-skew tolerance
    expectedIssuer: 'https://issuer.example.com',  // strict iss check (null = no check)
    expectedAudience: 'myapp',          // strict aud check (null = no check)
    requireExpiration: true,            // v0.3.0 default; reject tokens missing exp
    maxLifetimeSeconds: 3600,           // reject tokens with exp - iat > 3600 (1 hour cap)
    maxTokenLength: 8192,               // reject oversized tokens before parsing
);

$mapper = new WpClaimsUserMapper(
    idClaims: ['sub', 'user_id', 'uid', 'wp_user_id'],   // re-add 'sub' explicitly
);

$jwt = new JwtAuthMiddleware(
    verifier: $verifier,
    requiredScopes: ['api:read'],       // optional scope-based authorization
    userMapper: $mapper,
);

// Apply to a route group:
$router->group('/protected', function ($group) use ($jwt) {
    $group->get('/me', fn ($ctx) => Response::ok($ctx->user));
    $group->put('/me', $updateProfile)->protectedByMiddleware('jwt');
})->middleware($jwt);

For write routes, ALSO apply ->protectedByMiddleware() at the route layer so the WP permission layer doesn't deny by default. The middleware does the actual auth; ->protectedByMiddleware() just opens the WP gate.

3. JWT verifier flags (v0.3.0 reference)

| Constructor param | Default | Behavior | |---|---|---| | secret | required | HS256 secret. Compare against header alg HS256. | | leewaySeconds | 0 | Clock skew tolerance for nbf / exp checks. | | expectedIssuer | null | If set, iss claim must match exactly. | | expectedAudience | null | If set, aud claim must match exactly (string or in array). | | requireExpiration | true | Reject tokens without exp. Set to false only for migration. | | maxLifetimeSeconds | null | If set, reject tokens with exp - iat > maxLifetimeSeconds. | | maxTokenLength | 8192 | Reject oversized tokens before base64 decode (DoS guard). |

Verified at [Hs256JwtVerifier.php:21-25](Hs256JwtVerifier.php), enforcement at lines 42 (length), 116 (exp required), 140-145 (max lifetime), 154-180 (iss / aud).

3b. RS256/ES256 JWT via JWKS (v0.6.0)

For third-party OIDC/OAuth access tokens, use the dedicated JWKS verifier rather than a custom implementation:

use \BetterRoute\Middleware\Auth\BearerTokenAuthMiddleware;
use \BetterRoute\Middleware\Auth\JwtBearerTokenVerifierAdapter;
use \BetterRoute\Middleware\Jwt\HttpJwksProvider;
use \BetterRoute\Middleware\Jwt\Rs256JwksJwtVerifier;

$verifier = new Rs256JwksJwtVerifier(
    jwks: new HttpJwksProvider('https://issuer.example.com/jwks.json'),
    expectedIssuer: 'https://issuer.example.com',
    expectedAudience: 'my-api',
    allowedAlgorithms: ['RS256'],
);

$auth = new BearerTokenAuthMiddleware(
    verifier: new JwtBearerTokenVerifierAdapter($verifier),
    requiredScopes: ['api:read'],
);

Strict rules live in br-jwks-jwt-auth: kid must match exactly, none and HS* are rejected, HTTPS JWKS is required, and refresh() is attempted only once on key miss.

4. Custom bearer tokens (opaque, DB-backed)

use \BetterRoute\Middleware\Auth\BearerTokenAuthMiddleware;
use \BetterRoute\Middleware\Auth\BearerTokenVerifierInterface;

class MyTokenVerifier implements BearerTokenVerifierInterface
{
    public function verify(string $token): array
    {
        // Look up token in DB / third-party / cache
        $row = MyDb::get('SELECT user_id, scopes FROM api_tokens WHERE token = %s', $token);
        if ($row === null) {
            throw new \BetterRoute\Http\ApiException('Invalid token.', 401, 'invalid_token');
        }
        return [
            'sub'    => (string) $row->user_id,
            'scopes' => explode(' ', $row->scopes),
        ];
    }
}

$bearer = new BearerTokenAuthMiddleware(
    verifier: new MyTokenVerifier(),
    requiredScopes: ['orders:read'],
    userMapper: new WpClaimsUserMapper(idClaims: ['sub']),
);

$router->group('/api', function ($group) {
    // ...
})->middleware($bearer);

The verifier returns a "claims" array — same shape as JWT claims, even though the token is opaque. The downstream code (scope check, user mapping) is identical.

5. WordPress application passwords

use \BetterRoute\Middleware\Auth\ApplicationPasswordAuthMiddleware;

$appPwd = new ApplicationPasswordAuthMiddleware();

$router->group('/api', function ($group) {
    // ...
})->middleware($appPwd);

The middleware reads the standard WP Authorization: Basic header. Application passwords are WP 5.6+ native — users generate them in their profile, no plugin needed. Best for first-party tools and mobile apps with full WP user accounts.

6. Cookie + nonce (browser AJAX)

use \BetterRoute\Middleware\Auth\CookieNonceAuthMiddleware;

$cookieAuth = new CookieNonceAuthMiddleware(
    nonceAction: 'wp_rest',     // default — matches wpApiSettings.nonce
    requireNonce: true,
    requireLoggedIn: true,
);

$router->group('/internal', function ($group) {
    // ...
})->middleware($cookieAuth);

Client-side, attach the nonce:

fetch('/wp-json/myapp/v1/internal/widget', {
    headers: {
        'X-WP-Nonce': wpApiSettings.nonce,
    },
    credentials: 'same-origin',
});

wpApiSettings.nonce is automatically available when wp_enqueue_script('wp-api') runs OR when you've enqueued any script that depends on wp-api-fetch (which most Gutenberg / block editor pages do).

7. Combining middleware

Middleware chains run top-down. For "rate-limit before auth":

$router->group('/api', function ($group) { /* ... */ })
    ->middleware([$rateLimit, $jwt]);   // rate limit first, then auth

For "auth before rate limit" (so authenticated users get a higher rate limit):

->middleware([$jwt, $rateLimit])

The middleware order matters — RateLimitMiddleware's default key resolver returns the authenticated user's ID when the auth middleware ran first. Without auth-before-rate-limit, the rate limiter falls back to anonymous keys.

Critical rules

  • JWT exp claim REQUIRED by default (v0.3.0+). Pass requireExpiration: false only for migration scenarios.
  • Use br-jwks-jwt-auth for RS256/ES256 JWKS tokens (v0.6.0+). Do not write a fallback verifier that tries every key.
  • WpClaimsUserMapper does NOT include sub by default (v0.3.0+). Re-add it explicitly via idClaims: ['sub', ...] if your tokens use sub as user ID.
  • Set expectedIssuer AND expectedAudience for production JWT. Without them, any JWT signed with your secret is accepted, regardless of issuer / audience — no defense against cross-service token reuse.
  • maxLifetimeSeconds caps token age. A token with exp - iat = 86400 * 365 (a year-long token) bypasses your security policy if you don't cap.
  • maxTokenLength: 8192 is the DoS guard — rejects oversized tokens before parsing. Don't disable.
  • Always ->protectedByMiddleware() at the route layer for write routes when auth is via middleware — otherwise WP layer denies first (post-v0.4.0).
  • Middleware order matters. Auth-before-rate-limit lets the rate limiter key on user ID; reversed order keys on anonymous IP.
  • CookieNonceAuthMiddleware requires the client to send X-WP-Nonce. WP cookies alone aren't enough.
  • Custom bearer verifiers must throw ApiException (or return claims). Returning null / false is undefined behavior.

Common mistakes

// WRONG — JWT verifier without exp requirement, no iss/aud check
$verifier = new Hs256JwtVerifier(
    secret: 'key',
    requireExpiration: false,
);
// Anyone with the secret can issue eternal tokens for any audience.

// RIGHT — production-grade
$verifier = new Hs256JwtVerifier(
    secret: 'key',
    expectedIssuer: 'https://my-issuer.example.com',
    expectedAudience: 'my-api',
    requireExpiration: true,
    maxLifetimeSeconds: 3600,
);

// WRONG — middleware applied but WP layer still denies
$router->post('/secure', $handler)->middleware($jwt);   // (no protectedByMiddleware)
// → 403 from WP layer; middleware never runs

// RIGHT — declare both
$router->post('/secure', $handler)
    ->protectedByMiddleware('jwt')
    ->middleware($jwt);
// (or apply via group middleware which handles this automatically)

// WRONG — assuming sub maps to user ID by default
$verifier = new Hs256JwtVerifier(secret: $key);
$middleware = new JwtAuthMiddleware($verifier, userMapper: new WpClaimsUserMapper());
// Token has 'sub' = '5' (user ID 5) but mapper looks for 'user_id'. User is anonymous in the request context.

// RIGHT — explicit idClaims with sub
$mapper = new WpClaimsUserMapper(idClaims: ['sub', 'user_id']);

// WRONG — combining permission and protectedByMiddleware
$router->post('/foo', $handler)
    ->permission(static fn () => current_user_can('edit_posts'))
    ->protectedByMiddleware('jwt');
// permission overrides the __return_true that protectedByMiddleware sets — middleware delegation lost.

// RIGHT — pick one
$router->post('/foo', $handler)
    ->protectedByMiddleware('jwt');

// WRONG — middleware order with rate limit before auth (when you want per-user limits)
->middleware([$rateLimit, $jwt])
// → rate limit keys on anonymous IP because auth hasn't run yet.

// RIGHT
->middleware([$jwt, $rateLimit])

// WRONG — application passwords without HTTPS
$middleware = new ApplicationPasswordAuthMiddleware();
// On HTTP, the password is sent base64-encoded in plaintext over the wire.

// RIGHT — only deploy on HTTPS

// WRONG — sharing JWT secret across multiple services
$verifier = new Hs256JwtVerifier(secret: $sharedSecret);   // ← also used by 3 other services
// Any compromise of one service compromises all.

// RIGHT — per-service secret OR upgrade to RS256 / ES256 via Rs256JwksJwtVerifier

// WRONG — long-running tokens without rotation
maxLifetimeSeconds: 86400 * 30,   // 30-day tokens
// Plus no refresh-token flow → users with stolen tokens have a month of access.

// RIGHT — short-lived access tokens + refresh tokens
maxLifetimeSeconds: 3600,   // 1 hour
// (refresh-token implementation is your concern; better-route doesn't ship one)

Cross-references

  • Run br-routes for the route-layer ->protectedByMiddleware() mechanics — auth middleware needs it for v0.4.0+ writes.
  • Run br-jwks-jwt-auth for RS256/ES256 OIDC/OAuth JWT verification from JWKS.
  • Run br-hmac-signature for signed server-to-server/webhook endpoints where no user bearer token is involved.
  • Run br-rate-limiting for combining auth with rate limiting — middleware order matters.
  • Run br-error-contract for the standard error envelope — auth failures return 401 invalid_token shape.
  • Run br-install-and-migrate for the v0.3.0 JWT breaking changes (exp, sub) summary.

What this skill does NOT cover

  • Token issuance / refresh-token flow. Better-route VERIFIES tokens; issuance is your responsibility (use any standard JWT library on your auth server).
  • OAuth authorization-code, refresh-token, and client credentials flows. Better-route provides middleware primitives, not a complete OAuth server/client implementation.
  • WP-side capability mapping. Once authenticated, the user is mapped via WpClaimsUserMapper; subsequent permission checks use current_user_can against the mapped user.
  • Multi-factor auth. WP user accounts handle MFA via plugins; better-route consumes the resolved authenticated user.
  • API-key-style auth (single static keys per consumer). Use BearerTokenAuthMiddleware with a verifier that matches against your keys table.

References

  • JwtAuthMiddleware: [libraries/better-route/src/Middleware/Jwt/JwtAuthMiddleware.php:24-29](JwtAuthMiddleware.php) — __construct(verifier, requiredScopes, userMapper).
  • Hs256JwtVerifier: [libraries/better-route/src/Middleware/Jwt/Hs256JwtVerifier.php:18-26](Hs256JwtVerifier.php) — full constructor with v0.3.0 defaults.
  • Rs256JwksJwtVerifier: [libraries/better-route/src/Middleware/Jwt/Rs256JwksJwtVerifier.php](Rs256JwksJwtVerifier.php) — RS256/ES256 JWKS verifier added in v0.6.0.
  • HttpJwksProvider: [libraries/better-route/src/Middleware/Jwt/HttpJwksProvider.php](HttpJwksProvider.php) — HTTPS JWKS fetch + transient cache.
  • WpClaimsUser

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.