Modern web and mobile apps rely on OAuth 2.0 with OpenID Connect (OIDC) to authenticate users and authorize API calls. OIDC adds an identity layer to OAuth so that an Identity Provider (IdP) — such as Amazon Cognito, Okta, or Auth0 — can issue JSON Web Tokens (JWTs) that your backend can validate locally.
Each JWT is a compact, signed set of claims (who the user is, what they can access, when the token expires). To verify a JWT, your API needs the IdP’s public keys, which are typically published as a JWKS (JSON Web Key Set). These are discovered from the issuer (the iss claim) via the OIDC discovery document at:
https://<issuer>/.well-known/openid-configuration
which points to the JWKS endpoint (commonly .../.well-known/jwks.json).
In the past, security researchers have found issues like remote key injection via jku and OIDC discovery–driven SSRF. Along the same lines, we have found another interesting attack vector that leads to a JWT signature verification bypass allowing unauthorized access.
Key Concepts
OIDC — A standard layer on top of OAuth that defines how IdPs authenticate users and expose metadata (including where their JWKS lives).
JWT — A signed envelope of claims such as iss (issuer), sub (subject/user), aud (audience/your API), and exp (expiry).
JWKS — A JSON document containing the issuer’s public keys — your server uses these to verify JWT signatures.
The Vulnerability
Signature bypass occurs when an application dynamically pulls the public keys (JWKS) from the issuer and caches them, but implicitly trusts the endpoint received in the iss claim. In this implementation, a common (and insecure) approach is to:
- Read
issfrom the token - Make a network call to download the JWKS public keys
- Verify the token’s signature with those keys
Many quick-start guides point to exactly this pattern.
When an application follows this implementation, it leads to signature bypass: an attacker can forge a JWT signed with their own private key and set the iss claim to an attacker-controlled domain that serves a compliant JWKS. The verification then succeeds because it uses:
- The algorithm (
alg) from the JWT header - The signed content from the JWT body
- The signature from the JWT footer
- The public key fetched from the attacker’s JWKS
SignatureVerifier.verify(
token.header.alg,
token.signedPayload,
token.signature,
key_from(JWKS(iss))
)
How JWKS Is Retrieved by Common Providers
How JWKS is retrieved differs by provider, but the trust decision is the same:
- Amazon Cognito — the user pool ID differentiates your Cognito issuer (e.g.,
https://cognito-idp.<region>.amazonaws.com/<userPoolId>) - Auth0 — discovery at
https://{yourDomain}/.well-known/openid-configurationreturns ajwks_urispecific to your tenant - Okta — discovery at
https://{yourOktaDomain}/oauth2/default/.well-known/openid-configuration(or a custom AS) exposesjwks_uri
The issue occurs if the application implicitly trusts the discovery URL derived from the iss claim and uses libraries that automatically fetch and parse the JWKS from that unverified issuer.
Vulnerable Java/Spring Implementation
import com.nimbusds.jwt.SignedJWT;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import jakarta.servlet.http.HttpServletRequest;
@Bean
AuthenticationManagerResolver<HttpServletRequest> resolver() {
return request -> {
String token = bearer(request); // read from Authorization: Bearer ...
String iss = SignedJWT.parse(token)
.getJWTClaimsSet()
.getIssuer();
String jwks = iss + "/.well-known/jwks.json"; // ⚠️ trusts attacker-controlled iss
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withJwkSetUri(jwks)
.build();
JwtAuthenticationProvider provider = new JwtAuthenticationProvider(decoder);
return provider::authenticate;
};
}
How the Signature Bypass Works in Practice
- Generate a forged JWT using attacker-controlled private keys
- Expose a JWKS at an attacker domain (following the JWKS spec) under
/.well-known/jwks.json - Set the
issclaim in the forged token to the attacker domain - The vulnerable application reads
iss, discovers the JWKS from that URL, fetches the attacker’s public key, and “verifies” the token — granting access
Root Cause
Deriving the JWKS (and therefore trust) from unverified token data (
iss) instead of from pinned configuration (allowed issuers/tenants and their known JWKS endpoints).
Remediation
Never derive the JWKS URL from the iss claim in the token itself. Instead:
- Pin your issuer(s) — maintain a static allowlist of trusted issuers and their corresponding JWKS URIs in your application configuration
- Validate
issbefore fetching keys — check that the issuer matches your expected value before making any network calls - Use well-maintained libraries correctly — Spring Security’s
NimbusJwtDecoderis safe when configured with a hardcodedjwks-uri, not a dynamically constructed one
// ✅ Secure — issuer and JWKS URI are pinned in config, not derived from token
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder
.withJwkSetUri("https://your-idp.com/.well-known/jwks.json")
.build();
}
Found a JWT misconfiguration in your application? Get in touch — our application security team specialises in authentication and authorization assessments.
Want to secure your systems?
Talk to Our Team
Every engagement starts with a free conversation about your risk profile.