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
kidnever 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; echoAll 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.
| Style | Example value | Where you see it |
|---|---|---|
| JWK thumbprint (RFC 7638) | NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs | Many OIDC libraries and self-hosted issuers |
| Hex certificate or key hash | 943a3a5d7d919625a454e489b75c29adab57acba | |
| Base64url certificate thumbprint | sa3RgZQ_nZNVheAokCVqxY_8Cr4 | Microsoft Entra ID (same value as x5t) |
| Opaque provider-generated string | (varies by provider) | Many hosted identity providers |
| Human-readable label | signing-2026-09 | In-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.
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
kidup 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
kidthat resolves to an RSA public key must never be usable as an HMAC secret. - ·Ignore
jku,x5uand embeddedjwkheaders unless you have a specific, allow-listed need; they let the token nominate its own key. - ·Treat a missing or unknown
kidas 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
isswith 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
kidrecovers 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
kidwas reused. - ·Case or encoding differs.
kidcomparison 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
kidheader 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
kidmember of a JWK, used for matching. - ·RFC 7638: JWK thumbprints, a common way to derive
kidvalues. - ·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.