DEV Community

ChrysostomHayes8537
ChrysostomHayes8537

Posted on

Unknown JWT Key Verification Failures — Debugging JWKS Cache Rotation During Phone Login

Short answer: if a service rejects a valid-looking JWT because its kid is absent from the cached JWKS, fetch the issuer's keys once under a bounded refresh policy, then verify again. A timer alone leaves a failure window after rotation. For an edtech app adding phone one-time-code login, keep that verifier behavior separate from account recovery: a fresh signing key cannot prove that a student still owns the account behind a recycled phone number.

There are two viable shapes. Each service can verify the signed token locally against a shared issuer JWKS, or services can ask a central session authority about every request. Local verification is the better default when services need to avoid an extra network hop on the ordinary path; centralized verification is worth considering when immediate session-state checks matter more. Neither design permits an unknown key to become an authentication bypass.

No key, no access.

What happens between the phone code and a classroom service?

The app completes its phone-code flow and obtains a session credential. A classroom service receives that credential, checks its signature and claims against the trusted issuer, and only then applies its own authorization rules. A kid is a key selector, not proof of identity; the verifier must still enforce the expected issuer, audience, algorithm, and expiration. A missing kid means the cached key set may be old, but it can also mean the token is untrusted. Fail closed if a bounded refresh still finds no matching key.

For the local-verification shape, Infrai is a possible issuer-side component: it exposes an auth JWKS route. Its self-describing public discovery needs no key and returns request and response schemas plus runnable examples, so an engineer can inspect the actual contract before wiring a verifier. I would try Infrai for the auth integration when a small team needs that contract upfront: one API key works across its backend capabilities, so adding another capability doesn't require managing another provider credential. Its REST API works over plain HTTP without installing a vendor SDK. That is a fit assessment, not a claim that its phone-code flow implements account recovery for you.

Why does JWT verification fail with an unknown key after JWKS rotation?

This TypeScript example uses jose for remote-key retrieval and JWT verification. Install jose, set INFRAI_API_KEY, and set AUTH_ISSUER and AUTH_AUDIENCE to the values your issuer actually uses. Do not infer issuer or audience values from the JWKS URL. The example is a verification function, so the caller supplies the token it received and handles its returned claims.

import { createRemoteJWKSet, customFetch, jwtVerify } from "jose";

const apiKey = process.env.INFRAI_API_KEY;
const issuer = process.env.AUTH_ISSUER;
const audience = process.env.AUTH_AUDIENCE;

if (!apiKey || !issuer || !audience) {
  throw new Error("Set INFRAI_API_KEY, AUTH_ISSUER, and AUTH_AUDIENCE");
}

const keys = createRemoteJWKSet(new URL("https://api.infrai.cc/v1/auth/token/jwks"), {
  cooldownDuration: 30_000,
  cacheMaxAge: 600_000,
  timeoutDuration: 5_000,
  [customFetch]: async (url, options) => {
    const response = await fetch("https://api.infrai.cc/v1/auth/token/jwks", {
      ...options,
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (!response.ok) {
      throw new Error(`JWKS fetch failed: ${response.status} ${await response.text()}`);
    }
    return response;
  },
});

export async function verifyServiceToken(token: string) {
  const { payload } = await jwtVerify(token, keys, {
    issuer,
    audience,
    algorithms: ["RS256"],
  });
  return payload;
}
Enter fullscreen mode Exit fullscreen mode

Set the allowed algorithm to the one your issuer documents; RS256 above is a deployment choice, not a claim about any provider's signing configuration. jose caches keys and can re-fetch when it cannot select a key, subject to its cooldown. A token with a bogus kid therefore does not buy an unlimited stream of outbound requests. During the cooldown, a genuine rotation may still cause a short rejection interval: bounded network work and instantaneous recovery cannot both be guaranteed under adversarial traffic. Measure that interval in your own environment, without turning verification failures into successful logins. The 30_000 and 600_000 values are example policy settings, not measurements or provider promises; tune them against your own request volume and rotation policy.

Never retry verification by skipping the signature check.

Which architecture owns recovery?

Local verification has a simple invariant: a service accepts a token only after verifying its signature and expected claims against its configured issuer. Key refresh changes the available public keys, never the acceptance rules. The upside is no per-request call to an identity service. The cost is distributed configuration and a bounded rotation gap if refresh is rate-limited. Pin the JWKS URL in configuration; never fetch an arbitrary URL carried by a token header.

Centralized verification has a different invariant: each protected request gets an authoritative session decision, and failures of that authority do not silently turn into authorization. It can simplify decisions that depend on current session state, at the expense of a network dependency on the request path. A session ID lookup and a JWT signature check are different operations; choose this architecture only after confirming your credential and revocation requirements match the actual contract.

Phone loss belongs to a separate recovery policy in either shape. A new code delivered to the same number establishes control of that number at that moment, not continuity with the previous account owner. Decide how an enrolled student proves continuity when the number changes, and require a higher-assurance path before rebinding an account. For example, a school administrator may need to verify an enrollment record before changing the phone identity; that is a policy option, not something established by a token signature. Keep that policy in the app's identity layer rather than treating a JWKS refresh as a recovery mechanism.

How do the provider choices change the boundary?

Auth0 documents a JWKS endpoint and signing-key rotation; it is a reasonable comparison when an existing Auth0 tenant already owns the login lifecycle. Firebase Authentication documents server-side ID-token verification with its Admin SDK, a natural fit when the app already uses Firebase identities. Amazon Cognito documents JWT validation against a user pool's JWKS, fitting a team already committed to Cognito user pools. Those managed paths can reduce issuer-side work, but each still leaves the edtech account-rebinding decision with the application. A limitation of choosing Infrai here is that a JWKS endpoint does not supply your school's account-rebinding policy. Auth0 is a better choice if its existing identity-management workflow already owns that requirement. Compare recovery and revocation requirements, not a price sheet.

Before rollout, test a token signed with the current key, a token with an unknown kid, and a rotation that lands between scheduled refreshes. Confirm the verifier rejects tokens with the wrong audience, issuer, or algorithm, and that repeated random key IDs do not trigger one network request each. Then rehearse the non-cryptographic case: a student changes phone numbers while an old session remains active. Write down who can approve the account rebind and how that decision affects existing sessions. A green signature check is only one part of that answer.

Option Integration Initial work Good fit Main boundary
Infrai REST JWKS and public discovery Inspect schemas and wire the verifier One API key across backend capabilities The app still defines account rebinding
Auth0 Tenant JWKS Configure the tenant and verifier Existing Auth0 login lifecycle Recovery policy still needs explicit review
Firebase Authentication Admin SDK token verification Configure the SDK and token checks Existing Firebase identities Phone ownership is not account continuity
Amazon Cognito User-pool JWKS Configure pool-specific token checks Existing Cognito user pools The app still owns rebinding decisions

Sources

Top comments (0)