I use spring security in spring cloud gateway. cloud version is Finchely.SR2 and spring boot version is 2.0.x
Then i set two role in one matcher like this:
.pathMatchers("/apis/**").hasRole("TEST1")
.pathMatchers("/apis/**").hasRole("TEST2")
but, when i startup application and do test, just TEST1 role can access. TEST2 role get FORBIDDEN 403 response.
I know in spring-boot-starter-web with spring-security, it has any method like
.antMatchers("/apis/**").hasAnyRole("TEST1", "TEST2")
Does webflux spring security has any API like hasAnyRole(String...roles) to use?
There isn't one available in the base APIs from Spring Security, however I've been using the following for anyAuthority,
public class HasAnyAuthority<T> implements ReactiveAuthorizationManager<T> {
private final Collection<String> allowedAuthorities;
public HasAnyAuthority(Collection<String> allowedAuthorities) {
this.allowedAuthorities = allowedAuthorities;
}
#Override
public Mono<AuthorizationDecision> check(final Mono<Authentication> authentication,
T object) {
return authentication.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority).any(allowedAuthorities::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
}
with usage like,
.access(new HasAnyAuthority<>(allowedAuth.getAuthorities())
hasAnyRole and hasAnyAuthority will be available in WebFlux in Spring Security 5.2.0.
You can try them out now in 5.2.0.M3.
The syntax is the same
.pathMatchers("/apis/**").hasAnyRole("TEST1", "TEST2")
If it helps anyone
.pathMatchers("/apis/**")
.access((mono, context) -> mono.map(auth -> auth.getAuthorities().stream()
.filter(e -> (e.getAuthority().contains(TEST1) || e.getAuthority().contains(TEST2)))
.count() > 0)
.map(AuthorizationDecision::new))
Related
Overview
I am trying to write a program that accesses a public REST API. In order for me to be able to consume it, I need to provide an OAuth2 token.
My App uses Spring Boot 2.4.2 and Spring Cloud version 2020.0.1. The app itself does call the REST API once every 24h, download the data, and stores it in a database. A different microservice consumes this data at some other point and needs the data to have been refreshed daily.
My approach to this is to use OpenFeign to declare the REST Client that consumes the REST API and provide it an OAuth2 token. This is a problem that is quite common, so I assume that machine to machine client_credentials workflow is well documented.
And indeed, I did find a simple example to do this with OpenFeign - here: https://github.com/netshoes/sample-feign-oauth2-interceptor/blob/master/src/main/java/com/sample/feign/oauth2/interceptor/OrderFeignClientConfiguration.java
TL;DR: Trying to write a machine-to-machine microservice requiring an OAuth2 token (client_credentials grant type).
Problem
This was my first try, but unfortunately with the new Spring Security release, I can't seem to get the OAuth2FeignRequestInterceptor instantiated, I might have a package problem. I then went on to study the documentation for Spring Security and the new OAuth2 rewrite, which can be found here: https://docs.spring.io/spring-security/site/docs/5.1.2.RELEASE/reference/htmlsingle/#oauth2client.
Approaches
My approach is to use a RequestInterceptor which injects the current OAuth2 token into the request of the OpenFeign client, by adding an Authorization Bearer header. My assumption is that I can retrieve this, more or less automagically, using the Spring Security OAuth2 layer.
Using the documentation I tried providing a bean of OAuth2RegisteredClient to my interceptor, as well as a bean of type OAuth2AccessToken - which both didn't work. My last try looked like this and is to be seen, as a sort of hail mary, kind of approach:
#Bean
public OAuth2AccessToken apiAccessToken(
#RegisteredOAuth2AuthorizedClient("MY_AWESOME_PROVIDER") OAuth2AuthorizedClient authorizedClient) {
return authorizedClient.getAccessToken();
}
This doesn't work because RegisteredOAuth2AuthorizedClient requires a user session, lest it is null. I also saw someone else on Stackoverflow trying the same approach, but they actually did it in a Controller (=> Resolving OAuth2AuthorizedClient as a Spring bean)
I also tried some approaches that I have found here on SO:
Feign and Spring Security 5 - Client Credentials (Provided answer uses Spring Boot 2.2.4 - thus not relevant anymore)
Alternative For OAuth2FeignRequestInterceptor as it is deprecated NOW another gentleman looking for an alternative for OAuth2FeignRequestInterceptor
OAuth2FeignRequestInterceptor class deprecated in Spring Boot 2.3 - solution here again required an active user-session
https://github.com/jgrandja/spring-security-oauth-5-2-migrate this Github repo pops up every now and then, I studied it, but I deem it irrelevant to my question - maybe I missed something? From what I understood, this sample application has multiple providers using multiple scopes - but still a user that triggers a login and thus the automagic generation of an OAuth2 token through Spring Security. (also featured in this question: Migrating from Spring Boot Oauth2 to Spring Security 5) [1]
https://github.com/spring-cloud/spring-cloud-openfeign/issues/417 -> as of right now there is no replacement for OAuth2FeignRequestInterceptor
My assumption is that I can somehow use Spring Security 5 to solve this, but I simply can't wrap my head around how to actually do it. It seems to me that most of the tutorials and code samples I have found actually require a user-session, or are outdated with Spring Security 5.
It really seems that I am missing something and I hope that somebody can point me in the right direction, towards a tutorial or written documentation on how to achieve this.
In depth example
I tried supplying an OAuth2AuthorizedClientManager as seen in this example (https://github.com/jgrandja/spring-security-oauth-5-2-migrate).
For this, I registered an OAuth2AuthorizedClientManager following the example code:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
and provided it for my RequestInterceptor as can be seen here:
#Bean
public RequestInterceptor requestInterceptor(OAuth2AuthorizedClientManager clientManager) {
return new OAuthRequestInterceptor(clientManager);
}
Finally I wrote the interceptor, which looks like this:
private String getAccessToken() {
OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(appClientId)
// .principal(appClientId) // if this is not set, I receive "principal cannot be null" (or empty)
.build();
return Optional.ofNullable(authorizedClientManager)
.map(clientManager -> clientManager.authorize(request))
.map(OAuth2AuthorizedClient::getAccessToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElseThrow(OAuth2AccessTokenRetrievalException::failureToRetrieve);
}
#Override
public void apply(RequestTemplate template) {
log.debug("FeignClientInterceptor -> apply CALLED");
String token = getAccessToken();
if (token != null) {
String bearerString = String.format("%s %s", BEARER, token);
template.header(HttpHeaders.AUTHORIZATION, bearerString);
log.debug("set the template header to this bearer string: {}", bearerString);
} else {
log.error("No bearer string.");
}
}
When I run the code, I can see "FeignClientInterceptor -> apply called" output in the console, followed by an Exception:
Caused by: java.lang.IllegalArgumentException: servletRequest cannot be null
My assumption is that I receive this, because I don't have an active user session. It seems to me thus, that I absolutely need one to fix this problem - which I don't have in machine-to-machine communcations.
This is a common use-case so I am sure I must have made a mistake at some point.
Used packages
Maybe I made a mistake with my packages?
implementation 'org.springframework.boot:spring-boot-starter-amqp'
implementation 'org.springframework.boot:spring-boot-starter-jooq'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
According documentation need use AuthorizedClientServiceOAuth2AuthorizedClientManager instead of DefaultOAuth2AuthorizedClientManager
When operating outside of the context of a HttpServletRequest, use AuthorizedClientServiceOAuth2AuthorizedClientManager instead.
So. I was playing with your solution in my free time. And found the simple solution:
just add SecurityContextHolder.getContext().authentication principle to your code OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(appClientId).build();
Should be like this:
val request = OAuth2AuthorizeRequest
.withClientRegistrationId("keycloak") // <-- here your registered client from application.yaml
.principal(SecurityContextHolder.getContext().authentication)
.build()
Used packages:
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
application.yaml:
spring:
security:
oauth2:
client:
registration:
keycloak: # <--- It's your custom client. I am using keycloak
client-id: ${SECURITY_CLIENT_ID}
client-secret: ${SECURITY_CLIENT_SECRET}
authorization-grant-type: client_credentials
scope: openid # your scopes
provider:
keycloak: # <--- Here Registered my custom provider
authorization-uri: ${SECURITY_HOST}/auth/realms/${YOUR_REALM}/protocol/openid-connect/authorize
token-uri: ${SECURITY_HOST}/auth/realms/${YOUR_REALM}/protocol/openid-connect/token
feign:
compression:
request:
enabled: true
mime-types: application/json
response:
enabled: true
client.config.default:
connectTimeout: 1000
readTimeout: 60000
decode404: false
loggerLevel: ${LOG_LEVEL_FEIGN:basic}
SecurityConfiguration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration() : WebSecurityConfigurerAdapter() {
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
// #formatter:off
http
.authorizeRequests { authorizeRequests ->
authorizeRequests
.antMatchers(HttpMethod.GET, "/test").permitAll() // Here my public endpoint which do logic with secured client enpoint
.anyRequest().authenticated()
}.cors().configurationSource(corsConfigurationSource()).and()
.csrf().disable()
.cors().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.oauth2Client()
// #formatter:on
}
#Bean
fun authorizedClientManager(
clientRegistration: ClientRegistrationRepository?,
authorizedClient: OAuth2AuthorizedClientRepository?
): OAuth2AuthorizedClientManager? {
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
.builder()
.clientCredentials()
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(clientRegistration, authorizedClient)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
}
FeignClientConfiguration:
private val logger = KotlinLogging.logger {}
class FeignClientConfiguration(private val authorizedClientManager: OAuth2AuthorizedClientManager) {
#Bean
fun requestInterceptor(): RequestInterceptor = RequestInterceptor { template ->
if (template.headers()["Authorization"].isNullOrEmpty()) {
val accessToken = getAccessToken()
logger.debug { "ACCESS TOKEN TYPE: ${accessToken?.tokenType?.value}" }
logger.debug { "ACCESS TOKEN: ${accessToken?.tokenValue}" }
template.header("Authorization", "Bearer ${accessToken?.tokenValue}")
}
}
private fun getAccessToken(): OAuth2AccessToken? {
val request = OAuth2AuthorizeRequest
.withClientRegistrationId("keycloak") // <- Here you load your registered client
.principal(SecurityContextHolder.getContext().authentication)
.build()
return authorizedClientManager.authorize(request)?.accessToken
}
}
TestClient:
#FeignClient(
name = "test",
url = "http://localhost:8080",
configuration = [FeignClientConfiguration::class]
)
interface TestClient {
#GetMapping("/test")
fun test(): ResponseEntity<Void> // Here my secured resource server endpoint. Expect 204 status
}
I was working on getting a client credential flow with Auth0 to work using Spring Security 5.4.1. I created a little demo application for reference: https://github.com/mathias-ewald/spring-security-auth0-clientcredentials-demo
Everything works fine, but I was wondering how to handle multiple OAuth2 clients. As far as I understand, the configuration made in OAuth2ClientSecurityConfig is valid for all client credential flows to any provider, correct?
What if I have another provider and don't want to convert RequestEntity in the same way?
There's usually no perfect answer for multi-tenancy since a lot depends on how early in the request you want to fork the behavior.
In Spring Security's OAuth 2.0 Client support, the ClientRegistration is the tenant, and that tenant information is available in most of the client APIs.
For example, your Auth0RequestEntityConverter could have different behavior based on the ClientRegistration in the request:
public RequestEntity<?> convert(
OAuth2ClientCredentialsGrantRequest request) {
ClientRegistration client = request.getClientRegistration();
if (client ...) {
} else if (client ...) {
} ...
}
Or, if you need to configure more things than the request entity converter, you could instead fork the behavior earlier by constructing a OAuth2AuthorizedClientManager for each provider:
public class ClientsOAuth2AuthorizedClientManager implements OAuth2AuthorizedClientManager {
private final Map<String, OAuth2AuthorizedClientManager> managers;
// ...
public OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest request) {
String clientRegistrationId = request.getClientRegistrationId();
return this.managers.get(clientRegistrationId).authorize(request);
}
}
Changing the Order of the Spring Security WebFilter
I have an API Gateway implemented using Spring Cloud Gateway that uses Spring Security. Spring Security for WebFlux is implemented as a WebFilter right at the beginning of the filter chain. So after successful authentication the request would be forwarded to Spring Cloud Gateway's RoutePredicateHandlerMapping, which would try to deduce the destination based on the URL pattern, and then it would go to a FilteringWebHandler to execute the other filters of Spring Cloud Gateway.
My problem is the following: I have implemented a customized authentication algorithm which uses query string and header variables as credentials for authentication according to the requirements of the project, an this is working without any problem. The problem occurred when we needed to add a small customization for the authentication algorithm that is path independent. When the request reaches the WebFilter of Spring Security, pattern matching is not yet done so I do not know which application does it point to, for example:
app1:
-Path: /app1/**
app2:
-Path: /app2/**
Which means that instead of having authentication -> route mapping -> filtering web handler I should do route mapping -> authentication -> filtering web handler. Not that these three components are not similar, one of them is a filter another is a mapper and the last one is web handler. Now I know how to customize them but the problem is that I do not know how to intercept the Netty server building process in order to change the order of these operations. I need to wait for the building process to end and alter the content of the server before it starts. How can I do that?
EDIT: here is the final solution:
So here is how I did it:
Goal: removing the WebFilter of Spring Security from the default HttpHandler, and inserting it between RoutePredicateRouteMapping and the FilteringWebHandler of Spring Cloud Gateway
Why: Because I need to know the Application ID while carrying on my customized authentication process. This Application ID is attached to the request by the RoutePredicateRouteMapping by matching the request's URL to a predefined list.
How did I do it:
1- Removing the WebFilter of Spring Security
I created an HttpHandler bean that invokes the default WebHttpHandlerBuilder and then customize the filters. As a bonus, I removed unneeded filters in order to increase the performance of my API Gateway
#Bean
public HttpHandler httpHandler() {
WebHttpHandlerBuilder webHttpHandlerBuilder = WebHttpHandlerBuilder.applicationContext(this.applicationContext);
MyAuthenticationHandlerAdapter myAuthenticationHandlerAdapter = this.applicationContext.getBean(MY_AUTHENTICATED_HANDLER_BEAN_NAME, MyAuthenticationHandlerAdapter.class);
webHttpHandlerBuilder
.filters(filters ->
myAuthenticationHandlerAdapter.setSecurityFilter(
Collections.singletonList(filters.stream().filter(f -> f instanceof WebFilterChainProxy).map(f -> (WebFilterChainProxy) f).findFirst().orElse(null))
)
);
return webHttpHandlerBuilder.filters(filters -> filters
.removeIf(f -> f instanceof WebFilterChainProxy || f instanceof WeightCalculatorWebFilter || f instanceof OrderedHiddenHttpMethodFilter))
.build();
}
2- Wrapping Spring Cloud Gateway's FilteringWebHandler with Spring Web's FilteringWebHandler with the added WebFilter
I created my own HandlerAdapter which would match against Spring Cloud Gateway's FilteringWebHandler and wrap it with Spring Web's FilteringWebHandler plus the security filter I extracted in the first step
#Bean
public MyAuthenticationHandlerAdapter myAuthenticationHandlerAdapter() {
return new MyAuthenticationHandlerAdapter();
}
public class MyAuthenticationHandlerAdapter implements HandlerAdapter {
#Setter
private List<WebFilter> securityFilter = new ArrayList<>();
#Override
public boolean supports(Object handler) {
return handler instanceof FilteringWebHandler;
}
#Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
org.springframework.web.server.handler.FilteringWebHandler filteringWebHandler = new org.springframework.web.server.handler.FilteringWebHandler((WebHandler) handler, securityFilter);
Mono<Void> mono = filteringWebHandler.handle(exchange);
return mono.then(Mono.empty());
}
}
This way I could achieve better performance with highly customized HttpHandler pipeline that I suppose to be future-proof
END EDIT
Spring Security for WebFlux is implemented as a WebFilter which is executed almost as soon as a request is received. I have implemented custom authentication converter and authentication manager which would extract some variables from the header and URL and use them for authentication. This is working without any problem.
Now I needed to add another variable taken from RoutePredicateRouteMapping before authentication is done. What I want exactly is to remove the WebFilter (called WebFilterChainProxy) from its current position and put it between the RoutePredicateRouteMapping and the FilteringWeHandler.
Here is how the default process goes:
ChannelOperations calls ReactorHttpHandlerAdapter which calls HttpWebHandlerAdapter, ExceptionHandlingWebHandler, and then org.springframework.web.server.handler.FilterWebHandler.
This WebHandler would invoke its filters and then call the DispatchHandler. One of those filters is the WebFilterChainProxy that does the authentication for Spring Security. So first step is removing the filter from here.
Now the DispatchHandler which is called after the filters would invoke RoutePredicateHandlerMapping, which would analyze the routes and give me the route ID that I need, and then it would call the org.springframework.cloud.gateway.handler.FilteringHandler (this is not the same FilteringHandler above), and that in turn would call the other filters of the Spring Cloud Gateway. What I want here is to invoke the filter after RoutePredicatehandlerMapping and before org.springframework.cloud.gateway.handler.FilteringHandler.
What I ended doing was the following:
I created and WebHttpHandlerBuilder that would remove WebFilterChainProxy and pass it as a parameter to a customized DispatcherHandler. Now that the filter is removed the request would pass the first layers without requiring authentication. In my customized DispatcherHandler I would invoke the RoutePredicateHandlerMapping and then pass the exchange variable to the WebFilterChainProxy to do the authentication before passing it to the org.springframework.cloud.gateway.handler.FilteringHandler, which worked perfectly!
I still think that I'm over engineering it and I hope that there is a way to do it using annotations and configuration beans instead of all these customized classes (WebHttpHandlerBuilder and DispatcherHandler).
You should probably implement that security filter as a proper GatewayFilter, since only those are aware of the other GatewayFilter instances and can be ordered accordingly. In your case, you probably want to order it after the routing one.
Also, please don't cross-post, the Spring team is actively monitoring StackOverflow.
I had a similar problem. The accepted solution, while interesting, was a bit drastic for me. I was able to make it work simply by adding my custom filter before SecurityWebFiltersOrder.AUTHENTICATION in the security configuration. This is similar to what I've done with success in a regular Spring mvc application.
Here's an example using oauth authentication. tokenIntrospector is my custom introspector, and requestInitializationFilter is the filter that grabs the tenant id and stashes it in the context.
#AllArgsConstructor
#Configuration
#EnableWebFluxSecurity
public class WebApiGatewaySecurityConfiguration {
private final GatewayTokenIntrospector tokenIntrospector;
private final GatewayRequestInitializationFilter requestInitializationFilter;
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// #formatter:off
http
.formLogin().disable()
.csrf().disable()
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer.opaqueToken(c -> c.introspector(tokenIntrospector)))
.addFilterBefore(requestInitializationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
return http.build();
// #formatter:on
}
}
I am trying to build a web service using WebFlux. When I tried to configure Spring Security with Spring WebFlux, the requests are not intercepted by Spring Security. My Spring Security config is:
#EnableWebFlux
#EnableWebFluxSecurity
#Configuration
public class WebConfig {
#Bean
public MapUserDetailsRepository userDetailsRepository() {
UserDetails cust =
User.withUsername("user1").password("password")
.roles("USER").build();
UserDetails admin =
User.withUsername("admin1").password("password")
.roles("ADMIN").build();
return new MapUserDetailsRepository(cust, admin);
}
#Bean
public SecurityWebFilterChain springWebFilterChain(
HttpSecurity httpSecurity) {
return httpSecurity.authorizeExchange().anyExchange().
authenticated().and().build();
}
}
One way is to use method security. You need to add #EnableReactiveMethodSecurity to your configuration class and then secure handler component methods with annotations, such as #PreAuthorize("isAuthenticated()")
This is how I managed to get this working with Spring Boot 2.0.0.M4, but again this may depend on what kind of request handling you are doing.
This is an issue when using Spring Security with WebFlux. The workaround is posted here: https://jira.spring.io/browse/SPR-16144
In our Muhuru-Bay-Microgrid-Dashboad project we're using code from https://github.com/xpoft/spring-vaadin in an attempt to get Spring Boot and Vaadin to play nicely. The problem - with this approach we can't access many of the other rest service Spring Boot registers at startup such as
/configprops
/health
/dump
/info
/trace
/mappings
/error
/autoconfig
Our startup code looks like:
#Bean
public ServletRegistrationBean servletRegistrationBean() {
final ServletRegistrationBean servletRegistrationBean
= new ServletRegistrationBean(
new ru.xpoft.vaadin.SpringVaadinServlet(),
"/*", "/VAADIN/*");
return servletRegistrationBean;
}
When we try to access Spring Boot's registered REST services we get redirected to /error - which also doesn't work correctly. Any hints greatly appreciated.
Try to use this addon to integrate Spring Boot and Vaadin:
https://github.com/peholmst/vaadin4spring
It's still in beta, but in my opinion it works much better than the Xpoft addon.
Using https://github.com/peholmst/vaadin4spring with Spring Boot, I had the same problem of getting HTTP 404 when accessing the application's other REST services.
What worked for me was to set VaadinServletConfiguration.SERVLET_URL_MAPPING_PARAMETER_NAME in the spring environment to send the Vaadin UI to a different context path (/ui/*):
#SpringBootApplication
public class AppSpringConfig {
public static void main(String[] args) {
new SpringApplicationBuilder(AppSpringConfig.class).initializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
public void initialize(ConfigurableApplicationContext applicationContext)
{
ConfigurableEnvironment appEnvironment = applicationContext.getEnvironment();
Properties props = new Properties();
props.put(VaadinServletConfiguration.SERVLET_URL_MAPPING_PARAMETER_NAME, "/ui/*");
PropertySource< ? > source = new PropertiesPropertySource("vaadin", props);
appEnvironment.getPropertySources().addFirst(source);
}
}).run(args);
}
}