javaspringspring-bootkeycloaknimbus-jose-jwt

Keycloak integration with Spring boot error : Payload of JWS object is not a valid JSON object


Our project running on Spring boot 3.0.2 has issue integrating with keycloak 10.0.2 (3 yrs old version) for a particular client.

Below here are couple of dependencies:

pom :

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.2</version>
        <relativePath/>
</parent>

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

application.properties :

spring.security.oauth2.client.registration.keycloak.client-id=client1
spring.security.oauth2.client.registration.keycloak.client-secret=xxxxx-xxxxx
spring.security.oauth2.client.registration.keycloak.client-name=keycloak
spring.security.oauth2.client.registration.keycloak.scope=profile,openid,email
spring.security.oauth2.client.registration.keycloak.redirect-uri=https://localhost:9097/login/oauth2/code/keycloak
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/auth/realms/testrealm2
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/auth/realms/testrealm

Spring security config :

@Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf()
        .disable()
        .authorizeHttpRequests()
        .requestMatchers(getStaticResources(coralEnabled).toArray(new String[0]))
        .permitAll()
        .anyRequest()
        .authenticated()
        .and()
        .oauth2Login()
        .successHandler(kwAuthenticationSuccessHandler)
        .failureHandler(kwAuthenticationFailureHandler)
        .failureUrl("/login?error")
        .loginPage("/login")
        .permitAll()
        .and()
        .logout()
        .invalidateHttpSession(true)
        .logoutUrl("/logout")
        .logoutSuccessUrl("/login")
        .addLogoutHandler(
            new HeaderWriterLogoutHandler(
                new ClearSiteDataHeaderWriter(
                    ClearSiteDataHeaderWriter.Directive.CACHE,
                    ClearSiteDataHeaderWriter.Directive.COOKIES,
                    ClearSiteDataHeaderWriter.Directive.STORAGE)));
    return http.build();
  }

@Bean
  WebClient webClient(
      ClientRegistrationRepository clientRegistrationRepository,
      OAuth2AuthorizedClientRepository authorizedClientRepository) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
        new ServletOAuth2AuthorizedClientExchangeFilterFunction(
            clientRegistrationRepository, authorizedClientRepository);
    oauth2.setDefaultOAuth2AuthorizedClient(true);

    return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
  }

  @Bean
  public AuthorizationRequestRepository<OAuth2AuthorizationRequest>
      authorizationRequestRepository() {
    return new HttpSessionOAuth2AuthorizationRequestRepository();
  }

  @Bean
  public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
      accessTokenResponseClient() {
    DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
        new DefaultAuthorizationCodeTokenResponseClient();
    return accessTokenResponseClient;
  }

Keycloack version : 10.0.2

Spring boot web application is UI/thymeleaf based.

When the user clicks on the login button from UI, it fails at nimbus framework : invalid json when trying to parse.

Error :

com.nimbusds.jwt.proc.BadJWTException: Payload of JWS object is not a valid JSON object
    at com.nimbusds.jwt.proc.DefaultJWTProcessor.extractJWTClaimsSet(DefaultJWTProcessor.java:262) ~[nimbus-jose-jwt-9.24.4.jar!/:9.24.4]
    at com.nimbusds.jwt.proc.DefaultJWTProcessor.process(DefaultJWTProcessor.java:352) ~[nimbus-jose-jwt-9.24.4.jar!/:9.24.4]
    at com.nimbusds.jwt.proc.DefaultJWTProcessor.process(DefaultJWTProcessor.java:303) ~[nimbus-jose-jwt-9.24.4.jar!/:9.24.4]
    at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.createJwt(NimbusJwtDecoder.java:154) ~[spring-security-oauth2-jose-6.0.1.jar!/:6.0.1]
    at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.decode(NimbusJwtDecoder.java:137) ~[spring-security-oauth2-jose-6.0.1.jar!/:6.0.1]
    at org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider.getJwt(OidcAuthorizationCodeAuthenticationProvider.java:245) ~[spring-security-oauth2-client-6.0.1.jar!/:6.0.1]
    at org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider.createOidcToken(OidcAuthorizationCodeAuthenticationProvider.java:236) ~[spring-security-oauth2-client-6.0.1.jar!/:6.0.1]
    at org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider.authenticate(OidcAuthorizationCodeAuthenticationProvider.java:154) ~[spring-security-oauth2-client-6.0.1.jar!/:6.0.1]
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182) ~[spring-security-core-6.0.1.jar!/:6.0.1]
    at org.springframework.security.authentication.ObservationAuthenticationManager.lambda$authenticate$1(ObservationAuthenticationManager.java:53) ~[spring-security-core-6.0.1.jar!/:6.0.1]
    at io.micrometer.observation.Observation.observe(Observation.java:559) ~[micrometer-observation-1.10.3.jar!/:1.10.3]
    at org.springframework.security.authentication.ObservationAuthenticationManager.authenticate(ObservationAuthenticationManager.java:52) ~[spring-security-core-6.0.1.jar!/:6.0.1] 

Other oauth2 integrations like Azure AD, Google work fine, but with keycloak, notice some issue with parsing tokens.

After some debugging, we notice when the tokens are merged, there are two attributes with same name "sub", and hence invalid json.

{
   "exp":1675788223,
   "iat":1675787923,
   "auth_time":1675787563,
   "jti":"c07d2db2-xxxx",
   "iss":"https://login.idm.xxx.com/auth/realms/appid-xxx",
   "aud":"xxxx-dev",
   "sub":"f:5ce9dxxxx:user@user.com",
   "typ":"ID",
   "azp":"xxxx-dev",
   "nonce":"dH4EL-2zxxxx",
   "session_state":"fc0a7xxxx",
   "acr":"0",
   "sub":"user@user.com",
   "groups":[
      "FGM-AuthUser",
      "FGM-admin"
  }

Does someone have any idea ?

Integrating keycloak 10.0.2 (spring oauth2 config) with spring boot 3.0.2, and expect the authentication to work seamlessly, but fails during nimbus token parsing.


Solution

  • That particular Keycloak version has known issues with duplicate json keys in the JWT token (https://issues.redhat.com/browse/KEYCLOAK-14309).

    Duplicate keys in JSON are technically possible (although definitely not recommenced), as such you are at the mercy of the JWT decoder.

    Unfortunately, the Nimbus JWT decoder is strict about this as it will reject the JSON

    Invalid JSON: Unexpected duplicate key:sub at position 328.
    

    Your only options are :

            @Bean
            public JwtDecoder jwtDecoder() {
                // create and return your own jwtDecoder bean here.
                return jwtDecoder;
            }
    

    If you go for the custom jwtDecoder route you will need to register your custom decoder in your spring security context

    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange(exchanges -> exchanges
                .anyExchange().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .decoder(myCustomDecoder())
                )
            );
        return http.build();
    }