By AndyPublished

JWT kid Header: What the Key ID Means, Its Format and How to Use It Safely

kid is the key ID: a string in the JWT header that tells the verifier which key was used to sign the token. It is a JOSE header parameter defined in RFC 7515 §4.1.4, not a claim, so it sits next to alg and typ rather than in the payload. Its format is deliberately unspecified: it is a case-sensitive string that only has to match thekid of one key in the issuer's JWKS. It exists so that an issuer can publish several keys at once and rotate them without breaking verification. Because the verifier reads it before the signature has been checked, it is attacker-controlled input and must be treated as such.

JWT kid Meaning

RFC 7515 §4.1.4 describes kid as a hint indicating which key was used to secure the JWS. Three points in that definition matter in practice:

  • ·It is a hint. The verifier still has to confirm the key is one it trusts. A kid never introduces a new key; it only selects among keys you already hold.
  • ·The structure is unspecified. Any string works, as long as issuer and verifier agree.
  • ·It is optional. An issuer with exactly one key may leave it out. Once there are two keys, it becomes necessary in practice.

The matching field on the key side is the JWK kid parameter from RFC 7517 §4.5. When both exist, verification is a simple lookup: the header's kid equals the JWK'skid. See what is JWKS for the key side of that lookup.

Is kid a Header or a Claim?

People often search for the "kid claim", but kid is a header parameter. Claims such assub, aud and exp live in the payload and are defined by RFC 7519; kid lives in the protected header and is defined by RFC 7515 (JWS) and RFC 7516 (JWE). The distinction matters in code: libraries return it from the header, not from the decoded payload.

// Decoded header of a typical RS256 token
{
  "alg": "RS256",
  "kid": "943a3a5d7d919625a454e489b75c29adab57acba",
  "typ": "JWT"
}

Because the header is signed along with the payload, a verifier that checks the signature also confirms thatkid was not altered. But the lookup happens before that check, which is why the value has to be handled carefully. For the layout of the three segments, see header, payload and signature explained.

How to Decode and Read the kid

The header is the first base64url segment of the token. Paste the token into the jwtdecode.app decoder and the kid appears in the header panel, decoded locally in your browser. In code:

// Node.js (jose)
import { decodeProtectedHeader } from 'jose';
const { kid, alg } = decodeProtectedHeader(token);

// Node.js (jsonwebtoken)
import jwt from 'jsonwebtoken';
const { header } = jwt.decode(token, { complete: true });

# Python (PyJWT)
import jwt
header = jwt.get_unverified_header(token)
print(header.get("kid"))

# Shell
cut -d. -f1 <<< "$TOKEN" | tr '_-' '/+' | base64 -d 2>/dev/null; echo

All of these read the header without verifying anything. That is fine for key selection and debugging, and never enough to trust the token. The shell version may need = padding added for some lengths; decoding in JavaScript, Python and Go handles that properly.

JWT kid Format and Example Values

Since the RFC leaves the format open, providers have settled on a handful of styles. None is more correct than another; what matters is that the value is stable for a key and unique within the set.

StyleExample valueWhere you see it
JWK thumbprint (RFC 7638)NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9XsMany OIDC libraries and self-hosted issuers
Hex certificate or key hash943a3a5d7d919625a454e489b75c29adab57acbaGoogle
Base64url certificate thumbprintsa3RgZQ_nZNVheAokCVqxY_8Cr4Microsoft Entra ID (same value as x5t)
Opaque provider-generated string(varies by provider)Many hosted identity providers
Human-readable labelsigning-2026-09In-house issuers

If you are designing your own, the JWK thumbprint from RFC 7638 is a good default: it is derived from the public key itself, so it cannot drift out of sync with the key, and libraries compute it for you (calculateJwkThumbprint in jose). Avoid putting file paths, database keys or anything meaningful to your infrastructure into the value.

How Verifiers Use kid to Pick a Key

With a JWKS, the lookup is done for you. jose's createRemoteJWKSet and PyJWT'sPyJWKClient both match on kid and re-fetch the set when they see an unknown one. With keys you hold locally, do the lookup yourself against a fixed map:

import jwt from 'jsonwebtoken';

// Keys you trust, loaded from config or a secret store at startup.
const KEYS = {
  'signing-2026-09': process.env.JWT_KEY_2026_09,
  'signing-2026-06': process.env.JWT_KEY_2026_06,
};

function getKey(header, callback) {
  // Exact lookup in a fixed map. Unknown kid = reject.
  const key = Object.hasOwn(KEYS, header.kid) ? KEYS[header.kid] : undefined;
  if (!key) return callback(new Error('unknown kid'));
  callback(null, key);
}

jwt.verify(token, getKey, { algorithms: ['HS256'] }, (err, payload) => {
  // err.message: "error in secret or public key callback: unknown kid"
});

// Signing side: set the kid with the keyid option
jwt.sign(claims, KEYS['signing-2026-09'], { algorithm: 'HS256', keyid: 'signing-2026-09', expiresIn: '15m' });

Two details in that snippet are deliberate. Object.hasOwn stops akid of __proto__ or constructor from resolving to something on the object prototype. And the algorithm list is fixed by the verifier: thekid picks a key, it never picks the algorithm. Tie each key to one algorithm; the security best practices guide explains why.

ℹ
In PyJWT, set a kid when signing with jwt.encode(claims, key, algorithm="RS256", headers={"kid": "signing-2026-09"}). In jose, pass it to setProtectedHeader({ alg: 'ES256', kid }).

kid and Key Rotation

Rotation is the reason kid exists. Without it, a verifier holding two keys has to try both, and one holding only the new key rejects every token signed before the switch. With it, the rotation runs as an overlap:

  • ·Publish the new public key in the JWKS under a new kid, alongside the current key.
  • ·Wait at least as long as verifiers cache the JWKS, so everyone has the new key before it is used.
  • ·Switch signing to the new key. New tokens carry the new kid; old tokens still verify with the old one.
  • ·Retire the old key once the longest-lived token signed with it has expired.

Never reuse a kid for a different key. Verifiers cache keys by kid, so a reused value can leave some servers verifying with the old key material until their cache expires, which shows up as intermittent invalid signature errors on some instances only.

Security: kid Injection and How to Prevent It

The verifier reads kid from an unverified token and uses it to find a key. If that lookup is anything other than an exact match against a fixed set, the value can steer it. RFC 8725 §3.10 calls this out directly: applications should make sure the kid lookup does not create SQL or LDAP injection vulnerabilities. The documented patterns, which PortSwigger's Web Security Academy covers in its JWT labs, are:

Path traversal

Code that loads a key from disk using the kid as a filename, such asreadFile(`keys/${kid}`), can be pointed at a different file. If that file is empty or predictable and the algorithm is HMAC, whoever controls the kid knows the "secret" and can sign any token.

SQL injection

Code that builds a query from the kid by string concatenation can be made to return a key value the caller chose, with the same outcome.

Defences

  • ·Look the kid up in an in-memory map or a JWKS you fetched from a configured URL. Reject anything not found.
  • ·If keys must come from a database or filesystem, use parameterised queries, validate the value against a strict pattern (for example ^[A-Za-z0-9_-]{1,64}$) first, and never build paths from it.
  • ·Pin the algorithm per key. A kid that resolves to an RSA public key must never be usable as an HMAC secret.
  • ·Ignore jku, x5u and embedded jwk headers unless you have a specific, allow-listed need; they let the token nominate its own key.
  • ·Treat a missing or unknown kid as a failure, not as "use the default key".

To test your own service, sign a token with a kid that does not exist, one containing../, and one containing a quote character, and confirm each is rejected with the same generic error. The JWT attacks and vulnerabilities guide covers the related header-based attacks.

Common kid Value Problems

Most kid-related failures are not attacks but mismatches between the token and the key set the verifier loaded:

  • ·The kid is not in the JWKS at all. The token was issued by a different tenant, realm or environment. Compare the token's iss with the issuer whose JWKS you fetch; a staging token sent to production is the usual culprit.
  • ·The kid appeared minutes ago. The issuer rotated and your cache predates it. A verifier that re-fetches on an unknown kid recovers on its own; one that only refreshes on a timer fails until the timer fires.
  • ·The kid matches but the signature fails. The key is right but the algorithm is not, or the verifier is using a stale copy of a key whose kid was reused.
  • ·Case or encoding differs. kid comparison is exact and case-sensitive. Trimming, lower-casing or URL-decoding either side breaks matches.
  • ·The token has no kid. Fine for a single-key issuer, but a verifier holding several keys has to reject it or try each compatible key. Prefer issuers that always set it.

When you are debugging, decode the header, note the kid and alg, and search the issuer's JWKS for that exact string. That single check separates "wrong issuer or stale cache" from "wrong algorithm or key material" in a few seconds.

JWT kid RFC References

  • ·RFC 7515 §4.1.4: the kid header parameter for JWS.
  • ·RFC 7516 §4.1.6: the same parameter for JWE, where it identifies the key the content key was encrypted to.
  • ·RFC 7517 §4.5: the kid member of a JWK, used for matching.
  • ·RFC 7638: JWK thumbprints, a common way to derive kid values.
  • ·RFC 8725 §3.10: do not trust received claims, including the injection warning for kid.

Summary

kid is a JWT header parameter (RFC 7515 §4.1.4), not a claim. Its value is an opaque, case-sensitive string that matches the kid of one key in the issuer's JWKS, and its job is to make multiple keys and smooth rotation possible. Read it with decodeProtectedHeader,jwt.decode(token, { complete: true }) or jwt.get_unverified_header, look it up with an exact match against keys you already trust, pin the algorithm per key, and never let it reach a file path or an unparameterised query.

Ready to decode a token?
Use the free JWT decoder — paste any token for instant results, entirely in your browser.
Open JWT Decoder