Recently we were migrating our old application from Spring Security 5 to Spring security 6. The major problem we faced was there was no documentation available on internet for Spring Security 6. After struggling for a long time we were able to move our old application to latest version of spring which is 6.0 . Here I am documenting our finding in hope that it will be helpful for the readers.
The first class you need to write is SecurityConfiguration. The content of file is provided 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.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {
@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(AntPathRequestMatcher.antMatcher("/error")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/api/public/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/api/test/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/actuator/**")).permitAll()
.anyRequest().authenticated()
).sessionManagement(sm-> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(rs-> rs.jwt(j-> j.decoder(jwtDecoder)));
// @formatter:on
return http.build();
}
}
In the above class I defined a CorsFilter to allow all domains. CSRF is disabled and few URLs were allowed without authentication. We need to use OAuth2 for integrating Cognito pool JWT tokens, for that we specified jwtDecoder. The JwtDecoder is defined in next class:
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;
}
}
In the above class we are trying to decode the JWT token generated by Cognito pool which is passed to our API as bearer token. The JWT tokens are signed using a private key and validated using a public key. The public keys are provided as a JSON Web Key Set(JWKS) at the URL https://cognito-idp.<aws-region>.amazonaws.com/<cognito-pool-id>/.well-known/jwks.json by AWS. It contains public keys for validating tokens. There can be multiple keys in this set so you need to parse this JSON Web Key Set(JWKS) and pick the correct key as per header of JWT token.
After that you need to do some customization in claimSetConverter. The customization we required are that we want to use username of the cognito pool as user identification rather than UUID generated by cognito for the user. For doing that we overwritten value of "sub" with username received in claim. This value will be returned by principal.getName() when used in the code.
The second customization we did was to overwrite "scope" with "cognito:groups" because we want to use congnito groups as authority for authenticating our API calls. Assume cognito group name is ROLE_ADMIN then the authority it will be mapped to will be SCOPE_ROLE_ADMIN and we can use @PreAuthorize("hasAuthority('SCOPE_ROLE_ADMIN')")
for pre authorizing the API calls. One sample API is implemented below:
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Validated
@Slf4j
public class UserNameTestController {
@GetMapping("/userName")
@PreAuthorize("hasAuthority('SCOPE_ROLE_ADMIN')")
public ResponseEntity<String> getCurrentUserInfo(final Principal principal) {
return ResponseEntity.status(200).body(principal.getName());
}
}
The important maven dependencies used in this code are:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
<version>6.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>6.0.5</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.8.1</version>
</dependency>
I hope this will be helpful for the readers. Please drop me a comment if you need some more information.
References:
https://medium.com/javarevisited/json-web-key-set-jwks-94dc26847a34
No comments:
Post a Comment