I have a problem with my spring boot application (version 2.6.3).
I have configured reactive spring security like there:
MyApplication.java:
#SpringBootApplication
#EnableWebFlux
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class);
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http, final ReactiveOpaqueTokenIntrospector reactiveOpaqueTokenIntrospector) {
return http.authorizeExchange()
.anyExchange().authenticated()
.and()
.httpBasic().disable()
.cors().and()
.logout().disable()
.formLogin().disable()
.oauth2ResourceServer()
.opaqueToken()
.introspector(reactiveOpaqueTokenIntrospector)
.and().and()
.csrf()
.disable()
.build();
}
}
And this is my web resource (controller):
MyWebResource.java:
#RestController
public class MyWebResource implements MyWebResourceApi {
#PreAuthorize("hasRole('ROLE_USER')")
#Override
public Mono<String> details(String userId, ServerWebExchange exchange) {
return exchange.getPrincipal().map(Principal::getName);
}
}
It's work fine, when my access token is expired or incorrect the request should be denied. However when PreAuthorized allow request, my user principal will be never resolved in my exchange...
In reactive application authentication information is stored in the Reactive flow and accessible from Mono/Flux. You could use ReactiveSecurityContextHolder to obtain the currently authenticated principal, or an authentication request token.
#PreAuthorize("hasRole('ROLE_USER')")
public Mono<String> details() {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ((Principal) ctx.getAuthentication().getPrincipal()).getName());
}
I found this answer looking for a way to add the access token to my webclient requests. If we are using OAuth2 or OpenID Connect and want to access the token instead of the principal's name, then this is not possible via the principal in the security context.
Instead we need to create a ServerOAuth2AuthorizedClientExchangeFilterFunction and register it as a filter function to the WebClient:
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations,
authorizedClients);
oauth.setDefaultOAuth2AuthorizedClient(true);
where clientRegistrations is an injectabile bean of type ReactiveClientRegistrationRepository and authorizedClients is an injectable bean of type ServerOAuth2AuthorizedClientRepository.
We can then use the filters method for the builder to add our filter function to the exchangeFilterFunctions:
WebClient.builder()
.filters(exchangeFilterFunctions -> {
exchangeFilterFunctions.add(oauth);
})
.build();
Baeldung has a nice background article about this, which explains it in more detail: https://www.baeldung.com/spring-webclient-oauth2
Related
I have a spring application that exposes some webflux endpoints, I use a jwt token to authorize the post calls but we I also need the information given by the userinfo endpoint.
I have a SecurityWebFilterChain bean right now and we are using an oauth2ResourceServer configuration then calling the userinfoendpoint for further checks.
What is the best way to validate a jwt token then get the userinfo enpoint information for further validations?
ps: the authorization server is a third part one.
Security configuration without the external call for the user-info
#Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
.cors()
.and()
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
.oauth2Client()
.and()
.authorizeExchange()
.pathMatchers(HttpMethod.POST).authenticated()
.anyExchange().permitAll()
.and().oauth2ResourceServer().jwt()
;
return http.build();
}
The UserInfo Endpoint is part of OpenID Connect 1.0, and returns user information for an access token. It is not automatically called from a resource server (http.oauth2ResourceServer()) by Spring Security.
Based on your security configuration, it looks like you're wanting to use both OAuth2 Client (http.oauth2Client()) and OAuth2 Resource Server (http.oauth2ResourceServer()) in the same application. OAuth2 Client isn't designed for this use case (calling UserInfo from a resource server), and therefore would require customization to be adapted for such a case. Instead, you can simply use a RestTemplate or WebClient to call the UserInfo endpoint yourself.
You can do this in a custom Converter<Jwt, Collection<GrantedAuthority>> (or in this case the reactive version) like so:
#Configuration
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// #formatter:off
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.jwt((jwt) -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
// #formatter:on
return http.build();
}
private Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());
return converter;
}
private Converter<Jwt, Flux<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
JwtGrantedAuthoritiesConverter delegate = new JwtGrantedAuthoritiesConverter();
return (jwt) -> getUserInfo(jwt.getTokenValue())
.flatMapIterable((userInfo) -> {
Collection<GrantedAuthority> authorities = delegate.convert(jwt);
// TODO: Add authority from userInfo...
return authorities;
});
}
private Mono<Map<String, String>> getUserInfo(String accessToken) {
// TODO: Call user info and extract one or more claims from response
return Mono.just(new HashMap<>());
}
}
It is way more efficient to decode the data from a JWT than querying an external endpoint.
As a consequence, the best option is to configure the authorization-server (even if it is 3rd party) to enrich the JWTs (access and ID tokens) with the data you need for authorization. Most OIDC authorization-servers support it, just refer to its documentation (Keycloak, Auth0, Cognito, ...).
Once all the claims you need are in access-tokens, you can read it on resource-server from the JwtAuthenticationToken instance (or OAuth2AuthenticationToken for client app with oauth2login) in the security-context. This allows to write stuff like:
#PostMapping("/answers/{subject}")
#PreAuthorize("#userSubject == #auth.token.claims['sub']")
public ResponseEntity<String> addAnswer(#PathVariable("subject") String userSubject, #RequestBody #Valid AnswerDto, JwtAuthenticationToken auth) {
...
}
In one system, I generated the JWT token as follows:
List securityGroups = Arrays.asList("group1");
Map<String, Object> claims = Map.of("username", "user1", "securityGroups", securityGroups);
String token = Jwts.builder()
.setClaims(claims).setSubject("user1").setAudience("web")
.setIssuedAt(<date now>).setExpiration(<some expiration>)
.signWith(SignatureAlgorithm.HS512, "mysecret").compact();
In another application, I want to decrypt this token and set in the SecurityContext. First, I have security config as follows:
#Configuration
public class SecurityConf extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disabled().authorizeRequests()
.antMatchers("/principal").permitAll()
.anyRequests().authenticated();
}
}
With this, when user requests for an endpoint, say /books, the browser will receive 403 error.
Then I implement the /principal to use this token as an authenticated user in spring.
#RequestMapping("/principal")
public class PrincipalController {
#PostMapping
public void setPrincipal(#RequestBody String token) {
// i'm able to decrypt the token here
// use token to create principal
Authentication authentication = ....
SecurityContextHolder.getContext().setAuthentication(...)
}
}
I'm thinking once I set this in SecurityContext, for succeeding request where I attach the token in the Authorization header, I won't be getting anymore 403 or 401 error since user is authenticated and Spring knows that the token corresponds to the principal in the context. But this part I am not sure how to do it. Please advise.
You can achieve this with spring-security built-it JWT support for a server secured with JWT.
First, for a spring-boot application you'll need a dependency:
for Gradle:
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server:2.7.4'
for Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.7.4</version>
</dependency>
Then in any configuration class create beans of JwtDecoder type and a converter to convert some JWT claim to a collection of GrantedAuthority.
In your case it can be done like this:
#Bean
public JwtDecoder jwtDecoder() {
final SecretKey key = new SecretKeySpec("mysecret".getBytes(), JwsAlgorithms.HS512);
final NimbusJwtDecoder decoder = NimbusJwtDecoder.withSecretKey(key).build();
return decoder;
}
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
final JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("securityGroups");
grantedAuthoritiesConverter.setAuthorityPrefix("");
final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
Then just let your application know that you want to use this support by additional security filter chain configuration:
#Configuration
public class SecurityConf {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disabled().authorizeRequests()
.antMatchers("/principal").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer(oauth2Server -> oauth2Server.jwt());
return http.build();
}
}
Note that I don't use WebSecurityConfigurerAdapter because it's deprecated now.
Above configuration will create a filter which will try to authorize users before reaching your controller endpoints and will throw 401 if there's no JWT or it's expired or invalid for other reasons.
So you won't need a separate endpoint like "/principal" for "login" with a token, because Spring will create an Authentication object in a SecurityContextHolder for every request.
Moreover, this configuration will let you authorize users depending on their "securityGroups", so if you decide to configure some role-based or authority-based access to some endpoints Spring will check it for you and return 403 if authorities are insufficient.
I am trying to restrict specific endpoints on a Spring boot service depending on what role they have set in the OAuth2 credentials.
This is the endpoint
#RestController
#RequestMapping("/api/admin")
public class AdminController {
#GetMapping(produces = "application/json")
public TestResponse get() {
return new TestResponse("Admin API Response");
}
}
This is then secured using SecurityConfiguration bean
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.and()
.authorizeRequests()
.antMatchers("/login", "/", "/home", "/logout", "/ping").permitAll()
.antMatchers("/api/admin").hasRole("arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN")
.antMatchers("/api/user").hasRole("arn:aws:iam::xxxxxx:role/spring-sso-test-USER")
.and()
.oauth2Login()
.and()
.logout()
.logoutSuccessUrl("/logout");
}
}
I debugged the Principal and can see the correct IAM role in the list of attributes cognito:roles list
However when I hit the endpoint I get a HTTP 403 Unauthorized. Meaning that the user has authenticated successfully, but Spring does not recognize or understand the attributes or how to map them?
I tried using the #Secured annotation but that didn't change anything.
#Secured("arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN")
#GetMapping(produces = "application/json")
public TestResponse get() {
return new TestResponse("Admin API Response");
}
How do I allow this to work using an IAM role defined in AWS Cognito?
When you use the hasRole DSL method, Spring Security adds the ROLE_ prefix to your authority. So, the authority arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN will become ROLE_arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN.
You should use the hasAuthority method instead.
Additionally, you should take the cognito:roles from the attributes and add in the authorities, since it's the property that Spring Security will query to get the authorities.
To map the authorities you can use a OAuth2UserService:
#Bean
SecurityFilterChain app(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(this.oidcUserService())
...
)
);
return http.build();
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
// your custom implementation
}
More details in the documentation.
I have a reactive(Spring WebFlux) web-application where I am having few REST APIs which are protected resources.(Oauth2) . To access them manually, I need to get an authorization token with client credentials grant type and use that token in the request.
Now, I need to write tests where I can invoke the APIs by making a call through Spring's WebTestClient. I am getting 403 forbidden on trying to access the API. Where am I doing wrong when writing the test case.
Below is my security configuration:
#EnableWebFluxSecurity
public class WebSecurityConfiguration {
#Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeExchange()
.pathMatchers(ACTUATOR_ENDPOINT_PATTERN)
.permitAll()
.pathMatchers("/my/api/*")
.hasAuthority("SCOPE_myApi")
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
http.addFilterAfter(new SomeFilter(), SecurityWebFiltersOrder.AUTHORIZATION);
return http.build();
}
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder().filter(oauth).build();
}
}
Note:- I need this webclient bean because inside that filter (which I added to the SecurityWebFilterChain) I am calling another protected resource/API and the response of that API is being set in the reactive context
My application yaml:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${oidc-issuer-uri}
client:
provider:
myProvider:
issuer-uri: ${oidc-issuer-uri}
registration:
myProvider:
client-id: another-service-client
client-secret: ${another-service-clientSecret}
scope: anotherServiceScope
authorization-grant-type: client_credentials
My Controller:
#RestController
public class MyController {
#GetMapping(value = "/my/api/greet")
public Mono<String> greet() {
return Mono.subscriberContext()
.flatMap(context -> {
String someVal = context.get("MY_CONTEXT"); //This context is being set inside the filter 'SomeFilter'
//Use this someVal
return Mono.just("Hello World");
});
}
}
My Test Case:
#RunWith(SpringRunner.class)
#WebFluxTest(controllers = {MyController.class})
#Import({WebSecurityConfiguration.class})
#WithMockUser
public class MyControllerTest {
#Autowired
private WebTestClient webTestClient;
#Test
public void test_greet() throws Exception {
webTestClient.mutateWith(csrf()).get()
.uri("/my/api/greet")
.exchange()
.expectStatus().isOk();
}
}
Note:- I cannot bypass by not using my WebSecurityConfiguration class. Because the reactive context is being set in the filter which is added in the websecurityconfiguration.
2 things are required here:
First to access the /my/api/greet, the webTestClient needs SCOPE_myApi and since no "user" is involved here so we dont need #WithMockUser
#Test
public void test_greet() {
webTestClient
.mutateWith(mockOidcLogin().authorities(new SimpleGrantedAuthority("SCOPE_myApi")))
.get()
.uri("/my/api/greet")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("mockSasToken");
}
Next we need a wiremock server to mock the response of the "another service"
For this one option is to use spring boot #AutoConfigureWireMock(port = 0) to automatically boot up a wiremock server and shutdown for us at a random port.
Next we stub the response for the "another service" and the Oauth2 token endpoint in the test method.
Lastly, we need a "test" spring profile and a corresponding application-test.yaml where we tell spring to use the wiremock endpoints to fetch token:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:${wiremock.server.port}/.well-known/jwks_uri
client:
provider:
myProvider:
token-uri: http://localhost:${wiremock.server.port}/.well-known/token
registration:
myProvider:
client-id: mockClient
client-secret: mockSecret
I'm looking for the proper way to add role based authentication where I extract out roles from a JWT.
Ideally, I would like to extract roles from the JWT after authentication has taken place. This will work by inspecting the web token for some fields related to the roles which we are getting from our authentication system, keycloak.
My question is: is it possible to append roles to a request and then use http configuration to require one of these extracted roles?
Below is some relevant code that will help explain what I'm doing.
In my WebSecurityConfigurer, I make the access token available, scoped per request.
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public AccessToken accessToken() {
try {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
return ((KeycloakSecurityContext) ((KeycloakAuthenticationToken) request
.getUserPrincipal())
.getCredentials()).getToken();
} catch (Exception exc) {
return null;
}
}
Then I override some of the configuration of the http in the configure method.
http
// Disable session management
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// Allow calls to OPTIONS
.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.and()
// Authenticate every other call
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
Ideally, what I'd like to achieve is something like:
http.antMatchers("/foo").hasRole("jwt_extracted_role")
I am currently creating custom filters which extract roles from the token and then check for the correct roles, but this is maybe more cumbersome than it needs to be.
Any pointers on which methods of which configuration classes I should be looking to override to extract the roles from the request's, and add them to the request?
I ended up solving this by overriding the KeycloakAuthenticationProvider and providing my override class as a bean in the WebSecurityConfig. My class is below:
public class ResourceAwareAuthenticationProvider extends KeycloakAuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
... here I add granted authorities from the token's credentials ...
}
}
Then in my class WebSecurityConfigurer extends KeycloakWebSecurityConfigurerAdapter I override the AuthenticationProvider:
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return new ProviderManager(Lists.newArrayList(new ResourceAwareAuthenticationProvider()));
}
This allows me to do configuration like:
http.authorizeRequests()
.antMatchers("/**").hasAuthority("my-resource-authority")