Issue
I am trying to authenticate myself based on roles that I get in my claims. Basically I want to be able to connect to my application only if the specific roles are part of the claim
Dependencies :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.1.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>3.1.4</version>
<scope>compile</scope>
</dependency>
This is my security class :
@Configuration
@EnableWebSecurity(debug = true)
@AllArgsConstructor
public class SecurityOAuth2Config {
private static final Logger LOGGER = LogManager.getLogger(SecurityOAuth2Config.class);
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(AbstractHttpConfigurer::disable)
.headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(req -> req
.requestMatchers(antMatcher("/actuator/health")).permitAll()
.requestMatchers(antMatcher("/actuator/info")).permitAll()
.requestMatchers(antMatcher("/h2-console/**")).permitAll()
// Invoked internally inside the kubernetes cluster
.requestMatchers(antMatcher("/internal/api/**")).permitAll()
.requestMatchers(antMatcher("/api/**"))
.hasAnyRole("User1", "Admin3","TestDataUploader")
.anyRequest().permitAll()
)
.oauth2ResourceServer((oauth2) -> oauth2.jwt(jwtConfigurer ->
jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter())));
return http.getOrBuild();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
var grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("role");
grantedAuthoritiesConverter.setAuthorityPrefix("");
var jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
LOGGER.info("AUTHORITIES " + authorities);
authorities.forEach(authority -> {
if (authority instanceof OidcUserAuthority oidcAuth) {
oidcAuth.getUserInfo().getClaimAsStringList("role").forEach(
role -> mappedAuthorities.add(new SimpleGrantedAuthority("" + role)));
}
});
mappedAuthorities.addAll(authorities);
return mappedAuthorities;
};
}
}
I all the time get unauthorized
Bearer token example
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1699881658,
"iss": "https://id.test.io/",
"upn": "Jon",
"role": [
"Admin",
"Approve"
]
}
Controller endpoint example
@RestController
@RequestMapping("/api")
@AllArgsConstructor
public class ApplicationListController {
private ServiceApp serviceApp;
@GetMapping("/app")
public Collection<Response> getApplications() {
return serviceApp.loadResponse();
}
}
Application yaml file
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://id.test.io/
jwk-set-uri: https://id.test.io/.well-known/jwks.json
Solution
You should remove remove your GrantedAuthoritiesMapper bean, it duplicates what you already configured on the JwtAuthenticationConverter.
When using .hasAnyRole("User1", "Admin3","TestDataUploader"), you expect the authentication to contain one of ROLE_User1, ROLE_Admin3 or ROLE_TestDataUploader authorities, but you map the "role": [ "Admin", "Approve" ] claim without a prefix => authorities are Admin and Approve => no match because of missing ROLE_ prefix and because expected role is Admin3 (not Admin as contained in your claims). Three options:
- change access control rule to
.hasAnyAuthority("User1", "Admin","TestDataUploader") - change access control rule to
.hasAnyRole("User1", "Admin","TestDataUploader")and update yourJwtAuthenticationConverterwithgrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); - change the role name on your authorization server from
AdmintoROLE_Admin3(the claim in the token would be"role": [ "ROLE_Admin3", "ROLE_Approve" ])
Solution 1: with only "official" Boot starters
spring-boot-starter-oauth2-resource-server is enough, you don't need to explicitly depend on spring-boot-starter-security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
Update your Java conf as follow (implementation of the second bullet point above):
@Configuration
@EnableWebSecurity(debug = true)
@AllArgsConstructor
public class SecurityOAuth2Config {
private static final Logger LOGGER = LogManager.getLogger(SecurityOAuth2Config.class);
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationConverter jwtAuthenticationConverter) throws Exception {
http
.cors(AbstractHttpConfigurer::disable)
.headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(req -> req
.requestMatchers(antMatcher("/actuator/health")).permitAll()
.requestMatchers(antMatcher("/actuator/info")).permitAll()
.requestMatchers(antMatcher("/h2-console/**")).permitAll()
// Invoked internally inside the kubernetes cluster
.requestMatchers(antMatcher("/internal/api/**")).permitAll()
.requestMatchers(antMatcher("/api/**"))
.hasAnyRole("Admin", "User1", "TestDataUploader")
.anyRequest().permitAll()
)
.oauth2ResourceServer((oauth2) -> oauth2.jwt(jwtConfigurer ->
jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter)));
return http.getOrBuild();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
var grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("role");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
var jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
Keep the same properties (optionally remove jwk-set-uri which is most probably redundant in your case):
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://id.test.io/
# jwk-set-uri is not needed when
# - the resource server can reach .well-known/openid-configuration using issuer-uri
# - the iss claim in access tokens is exactly the value of issuer-uri (including case and trailing slash if any)
# jwk-set-uri: https://id.test.io/.well-known/jwks.json
Solution 2: using my starter & method security
This would probably make your life easier.
One more dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>7.1.13</version>
</dependency>
Replace spring.security.oauth2.resourceserver.* with the following:
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: https://id.test.io/
username-claim: upn
authorities:
- path: $.roles
prefix: ROLE_
resourceserver:
permit-all:
- /actuator/health
- /actuator/info
- /h2-console/**
- /internal/api/**
cors:
Clear your Java security conf and add @EnableMethodSecurity
@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity
public class SecurityOAuth2Config {
}
Yes, that simple, the stateless resource server filter-chain is created for you, based on the application properties above.
Your controller becomes (mind the @PreAuthorize("hasAnyRole('Admin', 'User1', 'TestDataUploader')")):
@RestController
@RequestMapping("/api")
@AllArgsConstructor
public class ApplicationListController {
private ServiceApp serviceApp;
@GetMapping("/app")
@PreAuthorize("hasAnyRole('Admin', 'User1', 'TestDataUploader')")
public Collection<Response> getApplications() {
return serviceApp.loadResponse();
}
}
Answered By - ch4mp
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.