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 yourJwtAuthenticationConverter
withgrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
- change the role name on your authorization server from
Admin
toROLE_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.