Most JWT vulnerabilities come from trusting the token’s own header: accepting alg: none, letting an attacker switch RS256 to HS256 (algorithm confusion), signing with a weak HMAC secret, and taking a key from an attacker-supplied jwk, jku or kid. You prevent them by pinning the algorithm and key on the server, rejecting unsigned tokens, using only the issuer’s known JWKS, and checking iss, aud and exp on every request.
Most write-ups on JWT attacks are written for the person attacking the API and leave the fix to a closing paragraph. This one is written for you, the person who owns the validation code. Each attack below gets a short annotated token that shows the trick, then the one validation rule that blocks it, with the recommendation from RFC 8725, JSON Web Token Best Current Practices behind it. At the end you get a checklist you can copy into a code review, library settings for jose, PyJWT, Nimbus and Spring, and the way a resource server should verify a Keycloak-issued token.
Key Takeaways
- Every classic JWT attack works by getting the verifier to let the token choose how it is checked. RFC 8725 section 3.1 says libraries “MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms” (RFC 8725).
- A valid signature only tells you who issued a token. The
audcheck tells you whether it was meant for your API, and RFC 8725 says a recipient “MUST reject the JWT” when the audience is missing or not its own (RFC 8725, section 3.9).- Three 2026 CVEs in WSO2, pac4j and Apache CXF show these flaws are still shipping in production identity stacks.
What is a JWT vulnerability?
A JWT vulnerability is a flaw in how an application creates or, much more often, verifies a JSON Web Token, so that a token the issuer never signed, or never meant for this API, is accepted as genuine. The JWT format itself, defined in RFC 7519, is not broken. The problems come from verifiers that read security decisions out of the token instead of out of their own configuration.
A quick reminder of the part that matters here. A signed JWT is a JSON Web Signature (RFC 7515) with three base64url parts: a header, a payload of claims, and a signature. The header says which algorithm (alg) was used and can also name or carry the key, through kid, jwk, jku and x5u. All of that header is written by whoever made the token, which during an attack is the attacker. In the examples below we show the decoded header and payload rather than the base64 string, so you can see what changed.
A token goes through five checks in order: the algorithm must be on your allow-list, the key must come from the issuer’s JWKS by kid, the signature must verify with that key, iss and aud must match exactly, and exp and nbf must be in range. Each check blocks a different family of attacks, and only a token that passes all five reaches your handler.
Which JWT attacks matter, and which check blocks each one?
This table is the short version of the rest of the post. Each row is one attack family, and the right-hand column is the rule in your validation code that stops it.
| Attack | What the attacker changes | The check that blocks it | RFC 8725 |
|---|---|---|---|
alg: none |
Header says the token is unsigned | Algorithm allow-list set on the server; never accept unsecured tokens | 2.1, 3.1, 3.2 |
| Algorithm confusion | RS256 switched to HS256, signed with the public key |
Allow-list plus one algorithm per key | 2.1, 3.1 |
| Weak HMAC secret | Nothing; they guess the secret offline | Random secret at least as long as the hash, or asymmetric keys | 2.2, 3.5 |
jwk, jku, x5u injection |
Header carries or points to the attacker’s key | Keys only from the issuer’s known JWKS URL | 3.8, 3.10 |
kid injection |
kid holds a file path or SQL |
Treat kid as a lookup key in a fixed set, never as a path or query |
3.10 |
| Wrong audience or issuer | A real token from another API or issuer | Exact iss match and your own identifier in aud |
3.8, 3.9 |
| Expired or future-dated token | Old or not-yet-valid token replayed | exp and nbf checked with a small clock skew |
(RFC 7519, 4.1.4, 4.1.5) |
| Wrong kind of token | An ID token sent as an access token | Explicit typing and mutually exclusive rules | 3.11, 3.12 |
How does the alg: none attack work?
The JWA specification, RFC 7518, defines none for an “Unsecured JWS”, a token with an empty signature. An attacker takes a real token, edits the claims and sets the header like this:
{ "alg": "none", "typ": "JWT" }
.
{ "sub": "1234", "role": "admin", "exp": 1924992000 }
.
(empty signature)
RFC 8725 describes the problem plainly: “The algorithm can be changed to ‘none’ by an attacker, and some libraries would trust this value” (section 2.1). RFC 7518 already says “Implementations MUST NOT accept Unsecured JWSs by default” (section 3.6), and most maintained libraries now follow it. The risk returns when someone calls a “decode” function instead of “verify”, or writes their own parser that branches on alg.
The rule that blocks it: configure the accepted algorithms on the server, and make sure none is not one of them. The OWASP JSON Web Token Cheat Sheet puts it as “Make sure that "alg":"none" is not accepted by your JWT parser.” Then add a test that sends an unsigned token and expects a 401.
How does JWT algorithm confusion (RS256 to HS256) work?
Algorithm confusion is the attack most people mean by “JWT vulnerability”. Your issuer signs with RS256, an RSA signature that uses a private key to sign and a public key to verify. The public key is public: anyone can download it from the issuer’s JWKS endpoint. The attacker changes the header to HS256, an HMAC that uses one shared secret for both signing and verifying, and signs the edited token using your RSA public key, byte for byte, as the HMAC secret:
{ "alg": "HS256", "typ": "JWT", "kid": "the-real-rsa-key-id" }
.
{ "sub": "1234", "role": "admin", "iss": "https://auth.example.com/realms/acme" }
.
HMAC-SHA256(header.payload, secret = your RSA public key bytes)
A vulnerable verifier looks up the key by kid, gets the public key, sees HS256 in the header, and runs an HMAC check with that key as the secret. The check passes, because the attacker used the same bytes. RFC 8725 lists exactly this case: “An ‘RS256’ (RSA, 2048 bit) parameter value can be changed into ‘HS256’ (HMAC, SHA-256), and some libraries would try to validate the signature using HMAC-SHA256 and using the RSA public key as the HMAC shared secret” (section 2.1).
The rule that blocks it: the same allow-list as for none, plus binding each key to one algorithm. RFC 8725 section 3.1 says “Each key MUST be used with exactly one algorithm, and this MUST be checked when the cryptographic operation is performed.” If your verifier only ever accepts RS256 for keys from your issuer, a token claiming HS256 fails before any key is touched. The Nimbus maintainers sum up the principle on their security page: “Never ever let the JWT header alone drive verification.”
Our post on CVE-2026-5430 and JWT algorithm confusion in WSO2 covers a related bug in WSO2 API Manager, and the diff of its public fix (carbon-apimgt #13752) shows the authentication interceptor catching the verification error, logging “Authentication Failure” and returning, and the fix replacing that with an exception that becomes an error response.
Why is a weak HMAC secret a JWT vulnerability?
If you sign with HS256 and a short or human-chosen secret, an attacker who has one valid token can try candidate secrets offline until the signature matches, and then sign any token they like. Nothing in the token changes, so no header check helps. RFC 8725 section 2.2 describes applications that “supply a weak symmetric key with insufficient entropy”, and section 3.5 rules that “human-memorizable passwords MUST NOT be directly used as the key to a keyed-MAC algorithm such as ‘HS256’” (RFC 8725).
The rule that blocks it: generate the secret randomly, and make it at least as long as the hash. RFC 7518 says “A key of the same size as the hash output (for instance, 256 bits for ‘HS256’) or larger MUST be used” (section 3.2). Better still, when the issuer and the API are different services, use an asymmetric algorithm such as RS256 or ES256, so the API only ever holds a public key and has no secret to leak or guess.
How do jwk, jku and x5u header injection attacks work?
RFC 7515 lets a token carry its verification key in the header (jwk, “the public key that corresponds to the key used to digitally sign the JWS”), or point to one by URL (jku, a JWK Set URL, and x5u, an X.509 certificate URL) (RFC 7515, section 4.1). An attacker generates their own key pair, signs a token with their private key, and puts their public key in the header:
{
"alg": "RS256",
"jwk": { "kty": "RSA", "e": "AQAB", "n": "attacker-public-key..." }
}
.
{ "sub": "1234", "role": "admin" }
.
RS256 signature made with the attacker's private key
With jku, the header holds a URL on a server the attacker controls instead. A verifier that uses whatever key the token supplies will find that the signature is valid, because it is, just not by your issuer. RFC 7515 already warned that “The key management technique employed to obtain public keys must authenticate the origin of the key; otherwise, it is unknown what party signed the message” (section 10.3). Following jku or x5u also makes your server fetch an attacker-chosen URL, and RFC 8725 notes that doing so blindly “could result in server-side request forgery (SSRF) attacks” (section 3.10).
The rule that blocks it: ignore jwk, jku and x5u in incoming tokens. Configure the issuer’s JWKS URL on your side, fetch keys only from there, and check that the key used belongs to the issuer named in iss, which RFC 8725 section 3.8 makes a MUST.
How does kid injection work?
kid is meant to be “a hint indicating which key was used to secure the JWS” (RFC 7515, section 4.1.4). Trouble starts when a verifier uses the hint as a file name or a database query. Two common shapes look like this:
{ "alg": "HS256", "kid": "../../../../dev/null" }
{ "alg": "HS256", "kid": "x' UNION SELECT 'attacker-secret' --" }
In the first, a verifier that reads the key from a file named by kid reads an empty file, and the attacker signs with an empty secret. In the second, a verifier that looks the key up in SQL returns a value the attacker chose. RFC 8725 section 3.10 asks applications to “ensure that this does not create SQL or LDAP injection vulnerabilities by validating and/or sanitizing the received value.”
The rule that blocks it: treat kid only as a key into a fixed set of keys you already hold, such as the cached JWKS from your issuer. If the kid is not in that set, refresh the set once from the configured URL and reject the token if it is still missing. Never build a path, query or URL from it.
Why are audience and issuer checks a defense, not an afterthought?
Every attack so far forges a token. Audience and issuer mistakes let an attacker use a real one. If your realm issues tokens to several APIs and an API checks only the signature, then a token a user obtained for a low-risk internal tool also works against your billing API. RFC 8725 calls this a substitution attack, in which a recipient is given a JWT “intended for it” and tries “to use it at a different recipient for which that JWT was not intended” (section 2.7).
The fix is two exact comparisons. RFC 8725 section 3.9 says the relying party “MUST validate the audience value, and if the audience value is not present or not associated with the recipient, it MUST reject the JWT.” The OWASP cheat sheet adds the detail that trips people up: validate iss “as a case-sensitive comparison of the whole string including scheme and path”, and check that “the recipient’s own identifier is present in aud, whether aud is a single string or an array of strings” (OWASP JWT Cheat Sheet).
Our write-up of Apache CXF CVE-2026-50627 covers a flaw whose CVE record says Apache CXF’s JwtAccessTokenValidator “fails to validate the ‘aud’ (Audience) claims of incoming JWT access tokens” (CVE-2026-50627), and the 401 audience errors behind Keycloak MCP servers show the same check from the other side, when a client asks for the wrong audience.
Expired, future-dated and wrong-kind tokens
exp is the time “on or after which the JWT MUST NOT be accepted for processing”, and nbf the time “before which the JWT MUST NOT be accepted” (RFC 7519, sections 4.1.4 and 4.1.5). Require exp to be present, check both, and allow only a small clock skew, usually well under a minute. If you need to end access sooner than exp, that is a revocation question, which our guide to access tokens and refresh tokens covers.
A related mistake is accepting the wrong kind of token. An ID token is signed by the same realm key as an access token, so a verifier that only checks signature and issuer will accept an ID token as an API credential. RFC 8725 section 3.12 says that when one issuer issues more than one kind of JWT, “the validation rules for those JWTs MUST be written such that they are mutually exclusive.” Keycloak access tokens carry "typ": "Bearer" in their payload (our JWT validation tutorial shows a full one), while in current Keycloak versions ID tokens carry "typ": "ID": the value is not spelled out in the documentation, but the code sets it in TokenManager from the constant in TokenUtil, and decoding one of your realm’s ID tokens shows it. Checking that claim keeps the two apart.
Patched on the day, not on your next maintenance window
Keycloak 26.7.2 fixed CVE-2026-18963, an account takeover through password reset. Skycloak had it available for auto-upgrade and told customers the same day upstream shipped it. Self-hosted teams schedule that work themselves.
Real JWT vulnerabilities in 2026 identity stacks
These flaws are not historical curiosities. Three 2026 CVEs in widely used Java identity and API products map directly onto the table above:
- WSO2 API Manager, CVE-2026-5430. WSO2’s advisory says JWT authentication “can be bypassed when a token is signed using an unsupported algorithm”, rated CVSS 10.0 (WSO2-2026-5328). The diff of the public fix (carbon-apimgt #13752) shows the verification error was caught and logged and the request then continued. It is the allow-list row, failed by error handling.
- pac4j-jwt, CVE-2026-29000. The vendor advisory tells users of the 4.x, 5.x and 6.x lines to upgrade to 4.5.9, 5.7.9 or 6.3.3 (pac4j), and the CVE record says versions before those “contain an authentication bypass vulnerability in JwtAuthenticator when processing encrypted JWTs that allows remote attackers to forge authentication tokens”, scored CVSS 3.1 9.1 by VulnCheck (CVE-2026-29000, published 2026-03-04). The same record says attackers with the server’s RSA public key “can create a JWE-wrapped PlainJWT with arbitrary subject and role claims, bypassing signature verification”, an encrypted token wrapping an unsigned inner token, which is the composition problem RFC 8725 section 2.3 warns about: libraries that decrypt a JWE “do not always validate the internal signature.”
- Apache CXF, CVE-2026-50627. The
JwtAccessTokenValidator“fails to validate the ‘aud’ (Audience) claims of incoming JWT access tokens”, so a token for one resource server could be replayed against another (CVE-2026-50627, announced on the Apache mailing list). That is the audience row.
One older case is worth knowing for a different reason. In 2022, Unit 42 reported CVE-2022-23529 in the Node.js jsonwebtoken library, and in January 2023 it worked with Auth0 to retract the CVE, noting that the issue needs the library to be “used in an insecure way” and that “the source of this risk in that case will be in the calling code” (Unit 42). The lesson still holds: a safe library does not help if your code lets the request decide which key or algorithm it is verified with.
How do you pin algorithms and keys in jose, PyJWT, Nimbus and Spring?
Every example below verifies a token from a Keycloak realm at https://auth.example.com/realms/acme for an API whose audience is orders-api. The JWKS URL is the realm’s certificate endpoint, /realms/your-realm/protocol/openid-connect/certs, which per the Keycloak documentation “returns the public keys enabled by the realm, encoded as a JSON Web Key (JWK).” Replace RS256 with your realm’s Default Signature Algorithm, the setting the Server Administration Guide lists under the realm’s Tokens tab as “The default algorithm used to assign tokens for the realm.”
Node.js with jose
In jose, algorithms “Defaults to all algorithms applicable to the key or secret” and “Unsecured JWTs (alg: "none") are never accepted”, so set the list explicitly rather than relying on the default:
import { createRemoteJWKSet, jwtVerify } from 'jose';
const issuer = 'https://auth.example.com/realms/acme';
const JWKS = createRemoteJWKSet(new URL(`${issuer}/protocol/openid-connect/certs`));
export async function verifyAccessToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ['RS256'], // allow-list, set on the server
issuer, // exact match, claim required
audience: 'orders-api', // your API's identifier, claim required
clockTolerance: 30, // seconds of skew for exp and nbf
requiredClaims: ['exp'],
});
if (String(payload.typ).toLowerCase() !== 'bearer') {
throw new Error('not an access token');
}
return payload;
}
createRemoteJWKSet only ever fetches the URL you gave it, so a jku in the token has no effect.
Python with PyJWT
PyJWT takes the algorithm list on decode, and its require option makes missing claims an error. PyJWKClient caches the realm’s key set and, per the docs, refreshes it from the endpoint and retries when a kid is not found in the current set:
import jwt
from jwt import PyJWKClient
ISSUER = "https://auth.example.com/realms/acme"
jwks = PyJWKClient(f"{ISSUER}/protocol/openid-connect/certs")
def verify_access_token(token: str) -> dict:
key = jwks.get_signing_key_from_jwt(token).key
claims = jwt.decode(
token,
key,
algorithms=["RS256"],
audience="orders-api",
issuer=ISSUER,
leeway=30,
options={"require": ["exp", "iss", "aud", "sub"]},
)
if str(claims.get("typ", "")).lower() != "bearer":
raise jwt.InvalidTokenError("not an access token")
return claims
Java with Nimbus JOSE + JWT
Nimbus “will not allow you to confuse unsecured JWTs with HMAC or digital signature protected JWTs, because it uses separate Java types for them” (Connect2id). Pin the algorithm in the key selector, and put issuer, audience and required claims in the claims verifier:
String issuer = "https://auth.example.com/realms/acme";
JWKSource<SecurityContext> jwkSource = JWKSourceBuilder
.create(new URL(issuer + "/protocol/openid-connect/certs"))
.build();
DefaultJWTProcessor<SecurityContext> processor = new DefaultJWTProcessor<>();
processor.setJWSKeySelector(
new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource));
processor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier<>(
"orders-api",
new JWTClaimsSet.Builder().issuer(issuer).claim("typ", "Bearer").build(),
Set.of("sub", "exp", "iat")));
JWTClaimsSet claims = processor.process(token, null); // throws on any failure
Spring Boot resource server
Spring Security’s NimbusJwtDecoder takes the algorithm when you build it, and its default validators check exp and nbf. Add issuer and audience on top:
@Bean
JwtDecoder jwtDecoder() {
String issuer = "https://auth.example.com/realms/acme";
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withJwkSetUri(issuer + "/protocol/openid-connect/certs")
.jwsAlgorithm(SignatureAlgorithm.RS256)
.build();
OAuth2TokenValidator<Jwt> audience = new JwtClaimValidator<List<String>>(
JwtClaimNames.AUD, aud -> aud != null && aud.contains("orders-api"));
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
JwtValidators.createDefaultWithIssuer(issuer), audience));
return decoder;
}
In all four, make sure the code around the call turns every exception into a 401. A correct verifier is still bypassed if a caught error lets the request continue, which is what happened in the WSO2 case above.
How should you fetch and cache JWKS without trusting jku or x5u?
The JWKS (JSON Web Key Set, RFC 7517) is the list of public keys your issuer currently signs with. Fetching it safely comes down to a few habits:
- Configure the URL, never read it from the token. Use the issuer’s discovery document (
/realms/your-realm/.well-known/openid-configurationin Keycloak) or thecertsURL directly, over HTTPS, and pin it in configuration. - Cache keys by
kid. The Keycloak documentation suggests you “lookup and cache the public key using the certificate endpoint with the Key ID (KID) embedded within the JWS” (Keycloak, OIDC layers), so you are not calling the issuer on every request. - Refresh on an unknown
kid, with a rate limit. When a token names akidyou do not have, refresh once, and limit how often a refresh can happen, so a stream of tokens with randomkidvalues cannot make you hammer the issuer. - Respect the key’s own
alganduse. If a JWK in the set declares"alg": "RS256"and"use": "sig", do not use it for anything else, which is the one-key-one-algorithm rule from RFC 8725 section 3.1. - Keep rotation overlap in mind. Keycloak’s Server Administration Guide describes rotation as “creating new keys with a higher priority than the existing active keys”, and suggests “creating new keys every three to six months and deleting old keys one to two months after you create the new keys.” A verifier that caches by
kidand refreshes on a miss picks up the new key without any change on your side.
Our explainer on JWKS and JSON Web Key Sets goes deeper on caching and rotation.
How does a resource server verify a Keycloak-issued token?
A resource server is the API that receives the bearer token. For Keycloak tokens, it either validates the token locally against the realm’s JWKS, as in the examples above, or asks Keycloak through the introspection endpoint. The Keycloak documentation notes that introspection requires a network call and “can be slow and possibly overload the server if you have too many validation requests going on at the same time,” which is why most APIs validate locally (Keycloak, OIDC layers). Our comparison of token introspection vs local validation weighs the trade-off, including when you need introspection to see revoked tokens.
For local validation, the Keycloak-specific points are these:
- Issuer:
https://your-host/realms/your-realm, compared as an exact string. - Algorithm: only the realm’s Default Signature Algorithm, the Tokens tab setting the Server Administration Guide describes as “The default algorithm used to assign tokens for the realm.” If you override the algorithm for one client, that client’s tokens need their own allow-list.
- Audience: give each API its own audience with an audience mapper or a dedicated client scope, then check it. Our guide to Keycloak token validation for APIs shows the mapper setup.
- Token kind: accept
typofBearerand reject ID tokens.
If you want a full walk-through with framework code, see how to verify a Keycloak-issued access token on the backend.
How do you test your own API for JWT vulnerabilities?
You do not need an attack framework to check the basics. Take one valid token from a test realm and send these variants to a staging copy of your API. Every one should get a 401, and none should reach your handler or produce a 500:
- The valid token with one payload character changed and the original signature.
- The header changed to
"alg": "none"and the signature removed. - The header changed to
"alg": "HS256"and signed with the realm’s public key as the HMAC secret. - A token signed with your own key pair, with that public key in a
jwkheader, and another with ajkupointing at a server you control. Watch your server’s outbound requests: it should never call that URL. - A
kidthat is not in the realm’s key set, and one containing../or a quote character. - A valid token issued for a different client or API in the same realm.
- An expired token, and one whose
nbfis an hour in the future. - A valid ID token from the same realm sent as the bearer token.
Decode a token in our JWT Token Analyzer first, so you can see its header and claims and know what you are changing. Turn the eight cases into automated tests, so a library upgrade or a refactor of your auth middleware cannot quietly reopen one of them.
The JWT validation checklist
Copy this into your code review template. Each line maps to one attack from the table at the top.
JWT validation checklist (resource server)
[ ] Accepted algorithms are set in server config (e.g. RS256 only); "none" is never accepted
[ ] Each key is bound to one algorithm; HS* is never accepted for keys from an asymmetric issuer
[ ] HMAC secrets, if any, are random and at least as long as the hash (256 bits for HS256)
[ ] Keys come only from the issuer's configured JWKS URL over HTTPS
[ ] jwk, jku and x5u headers in incoming tokens are ignored
[ ] kid is only a lookup into the cached key set; unknown kid triggers one rate-limited refresh, then reject
[ ] iss is compared to the expected issuer as an exact string
[ ] aud must contain this API's own identifier; missing aud is rejected
[ ] exp is required; exp and nbf are checked with under a minute of clock skew
[ ] Token kind is checked (typ Bearer for Keycloak access tokens); ID tokens are rejected
[ ] Every parsing, key lookup or verification error ends in 401, never in "log and continue"
[ ] The eight negative test tokens run in CI
For the wider set of habits around issuing and storing tokens, our JWT best practices for developers guide is the companion to this checklist.
Frequently asked questions
What is the most common JWT vulnerability?
The most common family is letting the token’s header decide how it is verified, which covers alg: none, RS256 to HS256 algorithm confusion, and jwk or jku injection. A server-side algorithm allow-list and keys taken only from the issuer’s JWKS block all of them, which is why RFC 8725 section 3.1 makes the allow-list a MUST for libraries.
Is HS256 insecure?
No. HS256 is safe with a random secret of at least 256 bits, as RFC 7518 section 3.2 requires. It becomes a problem with a short or guessable secret, and in algorithm confusion, where a verifier that expects RS256 is tricked into an HMAC check. When the issuer and the API are separate services, an asymmetric algorithm such as RS256 means the API holds no secret at all.
Does using a well-known JWT library protect me?
It helps a lot, because maintained libraries reject alg: none by default and let you set an algorithm list. It does not protect you if you call a decode-only function, leave the algorithm list at its default, skip aud and iss, or catch the verification error and continue. The 2026 WSO2 and Apache CXF CVEs were both in the code around the cryptography.
Can Keycloak itself be attacked with algorithm confusion?
Keycloak signs tokens and publishes its keys, and the attacks in this post target the services that verify those tokens. A resource server that pins the realm’s algorithm, fetches keys only from the realm’s certs endpoint and checks iss, aud, exp and typ is not open to them, whichever issuer signed the token.
What to do next
Pick one API that accepts JWTs, paste one of its tokens into the JWT Token Analyzer, and walk its validation code through the checklist above, then turn the eight test tokens into automated tests. If you would rather not run the issuing side yourself, Skycloak runs managed Keycloak clusters for you, so the realm that signs your tokens and publishes their keys is operated for you and your team only has to get the validation side right.
Sources
- IETF, “RFC 8725: JSON Web Token Best Current Practices”, sections 2.1, 2.2, 2.3, 2.7, 3.1, 3.2, 3.5, 3.8, 3.9, 3.10 and 3.12, retrieved 2026-10-30, https://datatracker.ietf.org/doc/html/rfc8725
- IETF, “RFC 7519: JSON Web Token (JWT)”, sections 4.1.4 and 4.1.5, retrieved 2026-10-30, https://datatracker.ietf.org/doc/html/rfc7519
- IETF, “RFC 7515: JSON Web Signature (JWS)”, sections 4.1.2 to 4.1.5 and 10.3, retrieved 2026-10-30, https://datatracker.ietf.org/doc/html/rfc7515
- IETF, “RFC 7518: JSON Web Algorithms (JWA)”, sections 3.2 and 3.6, retrieved 2026-10-30, https://datatracker.ietf.org/doc/html/rfc7518
- IETF, “RFC 7517: JSON Web Key (JWK)”, retrieved 2026-10-30, https://datatracker.ietf.org/doc/html/rfc7517
- OWASP Cheat Sheet Series, “JSON Web Token Cheat Sheet”, retrieved 2026-10-30, https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet.html
- Connect2id, “Nimbus JOSE + JWT: Security vulnerabilities”, retrieved 2026-10-30, https://connect2id.com/products/nimbus-jose-jwt/vulnerabilities
- WSO2, “Security Advisory WSO2-2026-5328/CVE-2026-5430”, retrieved 2026-10-30, https://security.docs.wso2.com/en/latest/security-announcements/security-advisories/2026/WSO2-2026-5328/
- WSO2, carbon-apimgt pull request #13752, retrieved 2026-10-30, https://github.com/wso2/carbon-apimgt/pull/13752
- CVE Program, “CVE-2026-29000”, published 2026-03-04, retrieved 2026-10-30, https://www.cve.org/CVERecord?id=CVE-2026-29000
- CVE Program, “CVE-2026-50627”, retrieved 2026-10-30, https://www.cve.org/CVERecord?id=CVE-2026-50627
- Palo Alto Networks Unit 42, “CVE-2022-23529” (with the January 2023 retraction note), retrieved 2026-10-30, https://unit42.paloaltonetworks.com/jsonwebtoken-vulnerability-cve-2022-23529/
- jose, “JWTVerifyOptions”, retrieved 2026-10-30, https://github.com/panva/jose/blob/main/docs/jwt/verify/interfaces/JWTVerifyOptions.md
- PyJWT, “Usage Examples”, retrieved 2026-10-30, https://pyjwt.readthedocs.io/en/stable/usage.html
- Keycloak, “Securing applications: OpenID Connect layers”, retrieved 2026-10-30, https://www.keycloak.org/securing-apps/oidc-layers
- Keycloak, “Server Administration Guide”, sections on realm keys and rotation, retrieved 2026-10-30, https://www.keycloak.org/docs/latest/server_admin/index.html
- Keycloak source, TokenManager.java and TokenUtil.java (token
typvalues), retrieved 2026-10-31, https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
