Search code examples
javaspring-bootspring-securityoauth-2.0spring-oauth2

JWTEncoder: Failed to select a JWK signing key


I have an auth-server + resource server in one app. I've spent a lot of time searching and debugging, but there aren't many updated pages or topics about Spring Boot 3.+ related to this. So, I had this working and wanted to add a custom secret that will be shared between my client and server. And here is where the problems started...

This is my auth+resource server config:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Value("${security.jwt.secret}")
private String jwtSecret;

@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
                                                           CorsConfigurationSource corsConfigurationSource) throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());

    http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(
                    new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
            .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));

    http.cors(customizer -> customizer.configurationSource(corsConfigurationSource));
    return http.build();
}

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(
                    authorize -> authorize.requestMatchers("/oauth2/authorize").permitAll().anyRequest().authenticated())
            .formLogin(formLogin -> formLogin.loginPage("/login").permitAll())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
    return http.build();
}

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

@Bean
public JwtEncoder jwtEncoder() {
    byte[] keyBytes = Base64.getDecoder().decode(jwtSecret);
    SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
    OctetSequenceKey octetKey = new OctetSequenceKey.Builder(secretKeySpec)
            .keyID("customKey")
            .build();
    JWKSet jwkSet = new JWKSet(octetKey);
    JWKSource<SecurityContext> jwkSource = (jwkSelector, context) -> {
        List<JWK> keys = jwkSelector.select(jwkSet);
        if (keys.isEmpty()) {
            System.out.println("No keys found matching selection criteria!");
        } else {
            System.out.println("Keys selected: " + keys.stream().map(JWK::getKeyID).collect(Collectors.joining(", ")));
        }
        return keys;
    };

    return new NimbusJwtEncoder(jwkSource);
}

@Bean
JwtDecoder jwtDecoder() {
    byte[] keyBytes = Base64.getDecoder().decode(jwtSecret);
    SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
    return NimbusJwtDecoder.withSecretKey(secretKeySpec).build();
}
}

And i have in my app.properties:

security.jwt.secret=r26BoWWyTQMp/8rkD3RnRKsbHkRsmQWjTvJTfmhrQxU=

I had everything working with asymmetric way (private and public key), but I wanted to try this wat too...

Now, when logging in with the client, I always receive:

org.springframework.security.oauth2.jwt.JwtEncodingException: An error occurred while attempting to encode the Jwt: Failed to select a JWK signing key

  1. What am i missing in the server?

Solution

  • I have fixed the issue with this:

    @Slf4j
    @Configuration
    @RequiredArgsConstructor
    @EnableWebSecurity
    public class SecurityConfig {
    
    @Value("${jwt.key}")
    private String jwtKey;
    
    private final TokenService tokenService;
    
    @Bean
    SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
                                                               CorsConfigurationSource corsConfigurationSource) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
    
        http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(
                        new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
                .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(jwtSpec -> {
                    jwtSpec.decoder(jwtDecoder());
                }));
    
        http.cors(customizer -> customizer.configurationSource(corsConfigurationSource));
        return http.build();
    
    }
    
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authz -> authz
                        .requestMatchers("/hello").authenticated()
                        .anyRequest().permitAll())
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt.decoder(jwtDecoder())))
                .formLogin(Customizer.withDefaults());
    
        return http.build();
    }
    
    @Bean
    AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }
    
    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers(new AntPathRequestMatcher("/h2-console/**"));
    }
    
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
        return tokenService.jwtCustomizer();
    }
    
    @Bean
    public JwtEncoder jwtEncoder() {
        return tokenService.jwtEncoder();
    }
    
    @Bean
    public JwtDecoder jwtDecoder() {
        byte[] keyBytes = Base64.getDecoder().decode(jwtKey);
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
        return NimbusJwtDecoder.withSecretKey(keySpec).build();
    }
    }
    

    And the TokenService class:

    @Service
    public class TokenService {
    
    @Value("${jwt.key}")
    private String jwtKey;
    
    public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
        return context -> {
            if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
                context.getJwsHeader().algorithm(MacAlgorithm.HS256);
                Date expirationDate = 
    Date.from(Instant.now().plus(Duration.ofHours(5)));
                Date issueDate = Date.from(Instant.now());
                context.getClaims().claims(claims -> {
                    claims.put("exp", expirationDate);
                    claims.put("iat", issueDate);
                    claims.put("custom", "custom");
                });
            }
        };
    }
    
    public JwtEncoder jwtEncoder() {
        return parameters -> {
            byte[] secretKeyBytes = Base64.getDecoder().decode(jwtKey);
            SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, "HmacSHA256");
    
            try {
                MACSigner signer = new MACSigner(secretKeySpec);
    
                JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder();
                parameters.getClaims().getClaims().forEach((key, value) ->
                        claimsSetBuilder.claim(key, value instanceof Instant ? Date.from((Instant) value) : value)
                );
                JWTClaimsSet claimsSet = claimsSetBuilder.build();
    
                JWSHeader header = new JWSHeader(JWSAlgorithm.HS256);
    
                SignedJWT signedJWT = new SignedJWT(header, claimsSet);
                signedJWT.sign(signer);
    
                return Jwt.withTokenValue(signedJWT.serialize())
                        .header("alg", header.getAlgorithm().getName())
                        .subject(claimsSet.getSubject())
                        .issuer(claimsSet.getIssuer())
                        .claims(claims -> claims.putAll(claimsSet.getClaims()))
                        .issuedAt(claimsSet.getIssueTime().toInstant())
                        .expiresAt(claimsSet.getExpirationTime().toInstant())
                        .build();
            } catch (Exception e) {
                throw new IllegalStateException("Error while signing the JWT", e);
            }
        };
    }
    }