top of page
Search

JWT Signature bypass via SSRF in iss claim

  • Avinash Malepati
  • Aug 17
  • 3 min read

Introduction:


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.


  • 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.

Signature bypass occurs when an application dynamically pulls the public keys (JWKS set) 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 iss from the token, make a network call to download the JWKS public keys, and then verify the token’s signature with those keys. Many quick start guides point to 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 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-configuration returns a jwks_uri specific to your tenant.

  • - Okta: discovery at https://{yourOktaDomain}/oauth2/default/.well-known/openid-configuration (or a custom AS) exposes jwks_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.



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";             
    NimbusJwtDecoder decoder = NimbusJwtDecoder
        .withJwkSetUri(jwks)
        .build();
    JwtAuthenticationProvider provider = new JwtAuthenticationProvider(decoder);    
    return provider::authenticate;
  };
}


How the signature bypass works in practice


1. Generate a forged JWT using attacker-controlled private keys.

2. Expose a JWKS at an attacker domain (following the JWKS spec) under /.well-known/jwks.json.

3. Set the iss claim in the forged token to the attacker domain.

4. The vulnerable application reads iss, discovers the JWKS from that URL, fetches the attacker’s public key, and “verifies” the token granting access.


That’s the 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).

 
 
 

Recent Posts

See All

Comments


appsecrew is a young consultant born of the experience and knowledge of its founder and the needs of our clients of all sizes and sectors.

Contact Info
  • Facebook
  • Twitter
  • LinkedIn
  • Instagram

Copyright © Appsecrew 2024. All Rights Reserved.

bottom of page