Recently we were trying to migrate our spring boot application to Java 21 and latest spring boot version to for enhancing security of our application. We hit major road blocks related to working of JWT tokens. Here I am documenting the solution which worked for us. Major idea is similar to my previous post https://blog.bigdatawithjasvant.com/2023/08/spring-security-60-with-oauth2-and-aws.html
But there were some new challenges which we faced. Let us get started.
We were having some public apis which were open to all without any authentication and some apis were secured with Cognito pool JWT token. This application was different from the one described in my previous post. So we were not building on top of application described in my previous post. For configuring access to apis you need to create a configuration class called SecurityConfiguration like below:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtDecoder jwtDecoder;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http.cors(c-> {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.setAllowedMethods(Arrays.asList("OPTIONS", "GET", "POST", "PUT", "DELETE", "PATCH"));
source.registerCorsConfiguration("/**", config);
c.configurationSource(source);
});
http.csrf(AbstractHttpConfigurer::disable);
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/","/api/public/**","/actuator/**").permitAll()
.anyRequest().authenticated()
).sessionManagement(sm-> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(rs-> rs.jwt(j-> j.decoder(jwtDecoder)));
// @formatter:on
return http.build();
}
}
For decoding JWT tokens we need to use JwtDecoder. The JWT decoder is defined in another config class like this:
import com.nimbusds.jose.*;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.MappedJwtClaimSetConverter;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import java.security.Key;
import java.util.*;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.text.ParseException;
@Configuration
public class JwtConfiguration {
public static final String COGNITO_GROUPS = "cognito:groups";
private static final String SPRING_AUTHORITIES = "scope";
public static final String COGNITO_USERNAME = "username";
private static final String SPRING_USER_NAME = "sub";
@Value("${security.oauth2.resource.jwk.key-set-uri}")
private String keySetUri;
@Bean
JwtDecoder jwtDecoder() throws ParseException {
// Obtain the JWKS from the endpoint
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> jwksResponse = restTemplate.getForEntity(keySetUri, String.class);
String jwksJson = jwksResponse.getBody();
JWKSet jwkSet = JWKSet.parse(jwksJson);
DefaultJWTProcessor<SecurityContext> defaultJWTProcessor = new DefaultJWTProcessor<>();
defaultJWTProcessor.setJWSKeySelector(new JWSKeySelector<SecurityContext>() {
@Override
public List<? extends Key> selectJWSKeys(JWSHeader header, SecurityContext context) throws KeySourceException {
RSAKey rsaKey = (RSAKey) jwkSet.getKeyByKeyId(header.getKeyID());
try {
return new ArrayList<Key>(Arrays.asList(rsaKey.toPublicKey()));
} catch (JOSEException e) {
e.printStackTrace();
}
return null;
}
});
NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(defaultJWTProcessor);
Converter<Map<String, Object>, Map<String, Object>> claimSetConverter = MappedJwtClaimSetConverter
.withDefaults(Collections.emptyMap());
jwtDecoder.setClaimSetConverter( claims -> {
claims = claimSetConverter.convert(claims);
HashMap<String, Object> hashMap = new HashMap<>(claims);
if (claims.containsKey(COGNITO_GROUPS))
((Map<String, Object>) hashMap).put(SPRING_AUTHORITIES, claims.get(COGNITO_GROUPS));
if (claims.containsKey(COGNITO_USERNAME))
((Map<String, Object>) hashMap).put(SPRING_USER_NAME, claims.get(COGNITO_USERNAME));
return hashMap;
});
return jwtDecoder;
}
}
When I tried running this code I faced a class not found exception which took a lot of time to resolve. The Exception was:
[ main] o.s.boot.SpringApplication , 857 : Application run failed java.lang.ClassNotFoundException: org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
... 57 common frames omitted
Wrapped by: java.lang.NoClassDefFoundError: org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationProvider
The reason of the problem was mismatch between different versions of spring security libraries. The problem was solved when I used following versions of the dependencies:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version> <!-- match AWS SDK requirements -->
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-oauth2-client -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
<version>6.5.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>6.5.2</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
<version>6.5.2</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>6.5.2</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>10.0.2</version>
</dependency>
</dependencies>
Please comment if you find it useful or need some more info.