How to Solve 403 Error in Spring Boot Post Request - spring

I am newbie in spring boot rest services. I have developed some rest api in spring boot using maven project.
I have successfully developed Get and Post Api. My GET Method working properly in postman and mobile. when i am trying to hit post method from postman its working properly but from mobile its gives 403 forbidden error.
This is my Configuration:
spring.datasource.url = jdbc:mysql://localhost/sampledb?useSSL=false
spring.datasource.username = te
spring.datasource.password = test
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5InnoDBDialect
Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
Please Suggest me how to solve error.

you have to disable csrf Protection because it is enabled by default in spring security: here you can see code that allow cors origin.
import org.springframework.context.annotation.Bean;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception{
http.cors().and().csrf().disable();
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

In Spring Security Cross-site check is by default enable, we need to disable it by creating a separate class to stop cross-checking.
package com.baba.jaxws;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
//we have stopped the csrf to make post method work
protected void configure(HttpSecurity http) throws Exception{
http.cors().and().csrf().disable();
}
}

Possible causes:
Requests done from postman are different to the one done from mobile (uri, method, headers)
Invalid token
CORS (read something about it, google is full of articles) add #CrossOrigin annotation to your controller.
mobile app is doing an OPTION request before performing the POST, and you block OPTION requests. If also from postman the OPTION requests are blocked, add the property spring.mvc.dispatch-options-request=true. Moreover, in case you are using spring security, you have to explicitly allow OPTION requests also for it.

I was able to solve this by using:
<form th:action="#{url}" method="post">
Instead of:
<form action="url" method="post">
It seems the th:action tag does url rewriting to enable csrf validation.

CSRF is enabled by default in Spring Security. Having this enabled ensures a 403 error on HTTP requests that would change (object) states.
For more information please visit: https://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/html5/#csrf
It is possible to disable CSRF in the Spring Security. However, it is enabled by default (convention over configuration) and for a good reason. This is also explained in the link provided to Spring's Security.
A working example, using Thymeleaf, might be:
HTML
<head>
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>
JS
function postExample() {
let token = $("meta[name='_csrf']").attr("content");
let header = $("meta[name='_csrf_header']").attr("content");
let data = {username: "", password: "", firstname: "", lastname: ""};
// Object key string interpolation by {[header]:token} works with ES6
fetch(window.location+"/addnote", {
method:"POST",
headers: {
[header]: token,
"charset": "UTF-8",
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}).then(res => console.log(res)).catch(err => console.log(err))
}
CONTROLLER per request of #mahmoud-magdy
#PostMapping("/addnote")
public Long addNote(#RequestBody() String data) {
Gson gson = new Gson();
JSONAddNote json = gson.fromJson(data, JSONAddNote.class);
return <service>.addNote(json.username, json....);
}
class JSONAddNote {
public String username;
public String ...etc
}
Or a more direct CONTROLLER:
#PostMapping("/addnote")
public Long addNote(#RequestBody Data data) {
return <service>.addNote(data);
}
class Data {
public String username;
public String ...etc
}

To build on the accepted answer
Many HTTP client libraries (eg Axios) implicitly set a Content-Type: JSON header for POST requests. In my case, I forgot to allow that header causing only POSTS to fail.
#Bean
CorsConfigurationSource corsConfigurationSource() {
...
configuration.addAllowedHeader("Content-Type"); // <- ALLOW THIS HEADER
...
}

This answer is related to this question if you are deploying to Open/WAS Liberty server.
If so, you might get 403 error even though your code works perfectly fine if deploying to embedded Tomcat that comes with Spring boot.
Liberty does not read (or considers) your
server.servlet.context-path=/myapi/v1
that you set in your application.properties or application.yml file for some reason. Or, it just overwrites it, not sure. So, the above context-path will work just fine if deployment in Spring Boot embeded Tomcat container.
However, when you deploy it to OpenLiberty/WASLiberty, you might find that your endpoints will stop working and you get 403 and/or 404 errors.
In my example, I have api where I have /auth endpoint in my WebSecurityConfiguration class:
//Customize the /login url to overwrite the Spring default provided /login url.
private AuthenticationFilter authenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
// This works fine on embedded tomcat, but not in Liberty where it returns 403.
// To fix, in server.xml <appllication> block, add
// <application context-root="/myapi/v1" ... and then both
// auth and other endpoints will work fine in Liberty.
filter.setFilterProcessesUrl("/auth");
// This is temporary "fix" that creates rather more issues, as it
// works fine with Tomcat but fails in Liberty and all other
// endpoints still return 404
//filter.setFilterProcessesUrl("/v1/auth");
return filter;
}
Based on the above context-path, on Tomcat, it becomes /myapi/v1/auth while on Liberty, it ends up being just /myapi/auth which is wrong. I think what Liberty does, it will just take the name of the api and add to it the endpoint, therefore ignoring the versioning.
As a result of this, AntPathRequestMatcher class matches() method will result in a non-matching /auth end point and you will get 403 error. And the other endpoints will result in 404 error.
SOLUTION
In your application.properties, leave
server.servlet.context-path=/myapi/v1
, this will be picked up by embedded Tomcat and your app will continue to work as expected.
In your server.xml configuration for Open/WAS Liberty, add matching context-root to the section like:
<application context-root="/myapi/v1" id="myapi" location="location\of\your\myapi-0.0.1.war" name="myapi" type="war">
, this will be picked up by Open/WASLiberty and your app will continue to work as expected on Liberty container as well.

Related

Multi-tenant Spring Webflux microservice with Keycloak OAuth/OIDC

Use Case:
I am building couple of reactive microservices using spring webflux.
I am using Keycloak as authentication and authorization server.
Keycloak realms are used as tenants where tenant/realm specific clients and users are configured.
The client for my reactive microservice is configured in each realm of Keycloak with the same client id & name.
The microservice REST APIs would be accessed by users of different realms of Keycloak.
The APIs would be accessed by user using UX (developed in React) publicly as well as by other webflux microservices as different client internally.
The initial part of the REST API would contain tenant information e.g. http://Service-URI:Service-Port/accounts/Keycloak-Realm/Rest-of-API-URI
Requirements:
When the API is called from UX, I need to invoke authorization code grant flow to authenticate the user using the realm information present in the request URI. The user (if not already logged in) should be redirected to the login page of correct realm (present in the request URI)
When the API is called from another webflux microservice, it should invoke client credential grant flow to authenticate and authorize the caller service.
Issue Faced:
I tried to override ReactiveAuthenticationManagerResolver as below:
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
#Component
public class TenantAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
private static final String ACCOUNT_URI_PREFIX = "/accounts/";
private static final String ACCOUNTS = "/accounts";
private static final String EMPTY_STRING = "";
private final Map<String, String> tenants = new HashMap<>();
private final Map<String, JwtReactiveAuthenticationManager> authenticationManagers = new HashMap<>();
public TenantAuthenticationManagerResolver() {
this.tenants.put("neo4j", "http://localhost:8080/realms/realm1");
this.tenants.put("testac", "http://localhost:8080/realms/realm2");
}
#Override
public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) {
return Mono.just(this.authenticationManagers.computeIfAbsent(toTenant(exchange), this::fromTenant));
}
private String toTenant(ServerWebExchange exchange) {
try {
String tenant = "system";
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (path.startsWith(ACCOUNT_URI_PREFIX)) {
tenant = extractAccountFromPath(path);
}
return tenant;
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
private JwtReactiveAuthenticationManager fromTenant(String tenant) {
return Optional.ofNullable(this.tenants.get(tenant))
.map(ReactiveJwtDecoders::fromIssuerLocation)
.map(JwtReactiveAuthenticationManager::new)
.orElseThrow(() -> new IllegalArgumentException("Unknown tenant"));
}
private String extractAccountFromPath(String path) {
String removeAccountTag = path.replace(ACCOUNTS, EMPTY_STRING);
int indexOfSlash = removeAccountTag.indexOf("/");
return removeAccountTag.substring(indexOfSlash + 1, removeAccountTag.indexOf("/", indexOfSlash + 1));
}
}
Then I used the overridden TenantAuthenticationManagerResolver class in to SecurityWebFilterChain configuration as 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.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
#Autowired
TenantAuthenticationManagerResolver authenticationManagerResolver;
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/health").hasAnyAuthority("ROLE_USER")
.anyExchange().authenticated()
.and()
.oauth2Client()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer()
.authenticationManagerResolver(authenticationManagerResolver);
return http.build();
}
}
Blow is the configuration in application.properties:
server.port=8090
logging.level.org.springframework.security=DEBUG
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.client-id=test-client
spring.security.oauth2.client.registration.keycloak.client-secret=ZV4kAKjeNW2KEnYejojOCsi0vqt9vMiS
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/keycloak
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/master
When I call the API using master realm e.g. http://localhost:8090/accounts/master/health it redirects the user to Keycloak login page of master realm and once I put the user id and password of a user of master realm, the API call is successful.
When I call the API using any other realm e.g. http://localhost:8090/accounts/realm1/health it still redirects the user to Keycloak login page of master realm and if I put the user id and password of a user of realm1 realm, the login is not successful.
So it seems that multi-tenancy is not working as expected and it is only working for the tenant configured in application.properties.
What is missing in my implementation w.r.t. multi-tenancy?
How to pass the client credentials for different realms?
I tried to use JWKS in place of client credentials but somehow it is not working. Below is the configuration used for JWKS in application.properties.
spring.security.oauth2.client.registration.keycloak.keystore=C:\\Work\\test-client.jks
spring.security.oauth2.client.registration.keycloak.keystore-type=JKS
spring.security.oauth2.client.registration.keycloak.keystore-password=changeit
spring.security.oauth2.client.registration.keycloak.key-password=changeit
spring.security.oauth2.client.registration.keycloak.key-alias=proactive-outreach-admin
spring.security.oauth2.client.registration.keycloak.truststore=C:\\Work\\test-client.jks
spring.security.oauth2.client.registration.keycloak.truststore-password=changeit
Note: JWKS is not event working for master realm configured in application.properties.
Need help here as I am stuck for many days without any breakthrough. Let me know if any more information is required.
Client V.S. resource-server configuration
Authentication is not the responsibility of the resource-servers: request arrive authorized (or not if some endpoints accept anonymous requests) with a Bearer access-token. Do not implement redirection to login there, just return 401 if authentication is missing.
It is the responsibility of OAuth2 clients to acquire (and maintain) access-tokens, with sometimes other flows than authorization-code, even to authenticate users: won't you use refresh-token flow for instance?
This OAuth2 client could be either a "public" client in a browser (your react app) or a BFF in between the browser and the resource-servers (spring-cloud-gateway for instance). The later is considered more secure because tokens are kept on your servers and is a rather strong recommendation lately.
If you want both client (for oauth2Login) and resource-server (for securing a REST API) configurations in your app, define two separated security filter-chains as exposed in this other answer: Use Keycloak Spring Adapter with Spring Boot 3
Multi-tenancy in Webflux with JWT decoder
The recommended way is to override the default authentication-manager resolver with one capable of providing the right authentication-manager depending on the access-token issuer (or header or whatever from the request): http.oauth2ResourceServer().authenticationManagerResolver(authenticationManagerResolver)
To easily configure multi-tenant reactive resource-servers (with JWT decoder and an authentication-manager switching the tenant based on token issuer), you should have a look at this Spring Boot starter I maintain: com.c4-soft.springaddons:spring-addons-webflux-jwt-resource-server. sample usage here and tutorials there. As it is open-source, you can also browse the source to see in details how this is done and what this reactive multi-tenant authentication-manager looks like.

Spring Boot Starter GraphQL OPTIONS Pre-Flight request rejected with HTTP 403

I've recently converted a GraphQL API from SpringBoot Web to WebFlux. In the previous version, the #RequestMapping was annotated with #CrossOrigin, which seemed to cover the OPTIONS HTTP verb.
In the new version, I am using the #MutationMapping / #QueryMapping annotations to map the schema to my methods, and responding to them reactively.
The problem is that for some of our frontend's, an OPTIONS preflight is sent and rejected with a 403 by this new implementation.
Is there an annotation or configuration I can enable where this preflight will not be rejected?
I've attempted to use the GraphQlWebFluxAutoConfiguration.GraphQlEndpointCorsConfiguration object however I can't seem to get it configured correctly. Any advice would be much appreciated, as I'm relatively new to the WebFlux stack.
Here's how I solved it - the issue was caused by my local dev environment having tomcat9, and the staging/qa environment I was deploying to having tomcat7 (not supporting WebFlux properly).
Please note that the app.enable-cors is a property defined in my application.properties file - you will need to add this if you are just copying+pasting this code.
I created two configuration classes, overriding both the Mvc and WebFlux Configurer.addCorsMapping(CorsRegistry registry) method. Depending on which auto configuration is loaded, the correct overriden method will be run. Code as follows:
First, the WebFlux version
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
#Configuration
#ConditionalOnProperty(name = "app.enable-cors", havingValue = "true")
public class GraphQLWebFluxConfiguration implements WebFluxConfigurer {
#Override
public void addCorsMappings(CorsRegistry corsRegistry){
corsRegistry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowedMethods("*");
}
}
Secondly the MVC version
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
#Configuration
#ConditionalOnProperty(name = "app.enable-cors", havingValue = "true")
public class GraphQLWebMvcConfiguration implements WebMvcConfigurer {
#Override
public void addCorsMappings(CorsRegistry corsRegistry){
corsRegistry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowedMethods("*");
}
}
I obviously have this disabled for the prod environment, but as a workaround for your QA/local environment it works well.

Apache Shiro is breaking the CORS configuration of my Spring Boot RestAPI

After tinkering with different security frameworks, I've decided to go with Apache Shiro for my Spring Boot RestAPI, because it appears to offer the necessary flexibility without too much bureaucratic overhead. So far, I haven't done anything except adding the maven dependency to my project:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.1</version>
</dependency>
This forced me to define a Realm bean in order to get the application started:
#Bean
public Realm realm() {
return new TMTRealm();
}
The bean pretty much does nothing yet, except for implementing the Realm interface:
public class TMTRealm implements Realm {
private static final String Realm_Name = "realm_name";
#Override
public String getName() {
return Realm_Name;
}
#Override
public boolean supports(AuthenticationToken token) {
return false;
}
#Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
So far so good. Except that now my RestAPI is violating CORS policy by not adding the 'Access-Control-Allow-Origin' header to any of its responses. I've noticed that Chrome doesn't send any dedicated OPTIONS request but two requests of the same method, GET in this case, with the first one failing as follows:
Access to XMLHttpRequest at 'http://localhost:8081/geo/country/names?typed=D&lang=en-US' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Without Shiro present it works perfectly fine, both the elegant way of using Spring's #CrossOrigin annotation on the controller and the brute force 'old school' way of defining a CorsFilter bean:
#Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("OPTIONS", "GET", "POST", "PUT", "DELETE"));
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
I've implemented Spring's ApplicationListener interface to hook into when the ApplicationContext is started and can thus see that the corsFilter bean is registered and present:
#Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("############ - application context started with beans: ");
final String[] beans = event.getApplicationContext().getBeanDefinitionNames();
Arrays.parallelSort(beans);
for (final String bean : beans) {
System.out.println(bean);
}
}
Output:
...
conditionEvaluationDeltaLoggingListener
conventionErrorViewResolver
corsFilter
countryController
...
But the filter is never called upon any request (I've set a breakpoint and System.out to prove it). I've also noticed that there are three Shiro beans present:
shiroEventBusAwareBeanPostProcessor
shiroFilterChainDefinition
shiroFilterFactoryBean
Therefore, I assume that probably the shiroFilterFactoryBean is breaking it somehow and needs extra attention and configuration. Unfortunately the Apache Shiro documentation doesn't seem to say anything about cross-origin requests, and I would assume that this is not (necessarily) part of Shiro's security concerns but rather of the underlying Restful API, that is Spring. Googling the issue didn't yield any helpful results either, so my suspicion is that I'm missing something big, or worse, something small and obvious here. While I'm trying to figure this out, any help or hint is greatly appreciated, thanks!
Awight, I figured it out. It's been a while that I was in filter-servlet land, so I didn't think of the order in which filters are executed. The naive way that I did it, the Shiro filter chain was always executed before my custom CorsFilter (and apparently the default Spring processor of #CrossOrigin annotation as well). Since I haven't yet configured Shiro yet, any request would be rejected as neither authenticated nor authorized, and so the CorsFilter was never executed causing a response without Access-Control-Allow-Origin header.
So, either I configure Shiro properly or just make sure to have the CorsFilter executed prior to the Shiro filter by using Spring's FilterRegistrationBean like this (setOrder to zero):
#Configuration
public class RestApiConfig {
#Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistrationBean() {
final FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>(this.corsFilter());
registration.setOrder(0);
return registration;
}
private CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("OPTIONS", "GET", "POST", "PUT", "DELETE"));
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public Realm realm() {
return new TMTRealm();
}
}
Edit:
D'uh, it was actually too easy for me to notice. All you have to do is to configure a ShiroFilterChainDefinition bean, so that it will refer to annotations on the controller classes or methods, like this:
#Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
final DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/**", "anon");
return chainDefinition;
}
Just like it is described in the documentation. Now it works both with a CorsFilter bean or with Spring's #CrossOrigin annotation. If there is no Shiro annotation present on the controller method, the request will be passed through.

Spring: forwarding to /oauth/token endpoint loses authentication

I'm building a Spring Boot authorization server which needs to generate Oauth2 tokens with two different auth methods. I want to have a different endpoint for each method, but by default Spring only creates /oauth/token, and while it can be changed, I don't think it is possible to have two different paths for it.
As an alternative, I'm trying to create two methods in a controller which do an internal forward to /oauth/token, adding a parameter to the request so I can know where it came from.
I have something like this:
#RequestMapping(value = "/foo/oauth/token", method = RequestMethod.POST)
public ModelAndView fooOauth(ModelMap model) {
model.addAttribute("method", "foo");
return new ModelAndView("forward:/oauth/token", model);
}
This performs the forward correctly, but the auth fails with:
There is no client authentication. Try adding an appropriate authentication filter.
The same request works correctly when sent to /oauth/token directly, so I'm guessing that the problem is that the BasicAuthenticationFilter is not running after the forward.
How can I make it work?
I had exactly the same issue. After some research I found out that the problem was caused by Spring Boot 2, not by Spring Security configurations. According to the Spring Boot 2.0 migration guide:
Spring Security and Spring Session filters are configured for ASYNC, ERROR, and REQUEST dispatcher types.
and the Spring Boot's SecurityFilterAutoConfiguration source code:
#Bean
#ConditionalOnBean(name = DEFAULT_FILTER_NAME)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
SecurityProperties securityProperties) {
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
DEFAULT_FILTER_NAME);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
return registration;
}
private EnumSet<DispatcherType> getDispatcherTypes(
SecurityProperties securityProperties) {
if (securityProperties.getFilter().getDispatcherTypes() == null) {
return null;
}
return securityProperties.getFilter().getDispatcherTypes().stream()
.map((type) -> DispatcherType.valueOf(type.name())).collect(Collectors
.collectingAndThen(Collectors.toSet(), EnumSet::copyOf));
}
where the defaults for securityProperties.getFilter().getDispatcherTypes() are defined in SecurityProperties as:
private Set<DispatcherType> dispatcherTypes = new HashSet<>(
Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
Thus by default, Spring Boot configures Spring Security so that its filters will not be applied to FORWARD requests (but only to ASYNC, ERROR and REQUEST), and therefore no security filter will be applied to authenticate the requests when forwarding them to /oauth/token.
The solution is simple. You can either add the following line to your application.properties in order to apply default filters to ALL forwarded requests
spring.security.filter.dispatcher-types=async,error,request,forward
or create your own custom filter chain with a path matcher and dispatcherType=FORWARD to only filter requests that are forwared to /oauth/token.
Looking carefully to the filter chains created for the Oauth endpoints, and for the forwarding controllers, it's easy to see that the latter are missing the BasicAuthenticationFilter, because they aren't authenticated, and auth isn't performed again after the forward.
To solve it, I created a new config like this:
#Configuration
public class ForwarderSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();
#Autowired
private FooClientDetailsService fooClientDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
for (AuthorizationServerConfigurer configurerBit : configurers) configurerBit.configure(configurer);
http.apply(configurer);
http
.authorizeRequests()
.antMatchers("/foo/oauth/token").fullyAuthenticated()
.and()
.requestMatchers()
.antMatchers("/foo/oauth/token");
http.setSharedObject(ClientDetailsService.class, fooClientDetailsService);
}
}
This code mimics what Spring Oauth does behind the scenes (here), running identical filter chains with the same authentication options on both endpoints.
When the /oauth/token endpoint finally runs, it finds the auth results that it expects, and everything works.
Finally, if you want to run a different ClientDetailsService on two forwarding endpoints, you just have to create two configuration classes like this one, and replace the ClientDetailsService on the setSharedObject call in each of them. Note that for this, you'll have to set different #Order values in each class.

Spring #FeignClient with OAuth2FeignRequestInterceptor not working

I'm trying to set FeignClient with OAuth2 to implement "Relay Token". I just want FeignClient to relay / propagate the OAuth2 Token that comes from ZuulProxy (SSO Enabled).
I use Spring 1.3.1-RELEASE and Spring Cloud Brixton.M4.
I have added an interceptor in a custom #FeignClient configuration:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import feign.RequestInterceptor;
#Configuration
public class FeignClientConfiguration {
#Value("${security.oauth2.client.userAuthorizationUri}")
private String authorizeUrl;
#Value("${security.oauth2.client.accessTokenUri}")
private String tokenUrl;
#Value("${security.oauth2.client.client-id}")
private String clientId;
// See https://github.com/spring-cloud/spring-cloud-netflix/issues/675
#Bean
public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oauth2ClientContext){
return new OAuth2FeignRequestInterceptor(oauth2ClientContext, resource());
}
#Bean
protected OAuth2ProtectedResourceDetails resource() {
AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails();
resource.setAccessTokenUri(tokenUrl);
resource.setUserAuthorizationUri(authorizeUrl);
resource.setClientId(clientId);
// TODO: Remove this harcode
resource.setClientSecret("secret");
return resource;
}
}
And I add the configuration to my #FeignClient like that:
#FeignClient(name = "car-service", configuration = FeignClientConfiguration.class)
interface CarClient {
#RequestMapping(value = "car-service/api/car", method = GET)
List<CarVO> getAllCars();
}
The application starts but when I use the Feign Client from my service I get:
2016-01-08 13:14:29.757 ERROR 3308 --- [nio-9081-exec-1] o.a.c.c.C.[.[.[. [dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in
context with path [/user-service] threw exception [Request processing failed; nested exception is com.netflix.hystrix.exception.HystrixRuntimeException: getAllCars failed and no fallback available.] with root cause
java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
at org.springframework.web.context.request.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.java:41) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:340) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
I want my application / microservice (the one that uses the #FeingClient to call the other application / microservice) to be STATELESS. However, I have tried both, with security.sessions=STATELESS (SpringBoot default) and security.sessions=ALWAYS (just to try).
In both cases I got the same exception.
Having a look at the code I have seen that the OAuth2ClientContext is saved in Session (Session scoped bean). How does it work when you want to implement a STATELESS OAuth2 enabled application / microservice? Precisely this is one of the big advantages of using OAuth2 in my current scenario. However, as I said, the result was the same enabling sessions.
Can someone help with this, please?
Thanks so much! :-)
I have found out that the problem is that Hystrix forces code execution in another thread and so you have no access to request / session scoped beans.
I was using #FeignClient with Hystrix enabled. When I disable Hystrix using feign.hystrix.enabled: false
the call from Microservice A to Microservice B relaying the token (using OAuth2FeignRequestInterceptor) works fine.
However, it would be desirable to be able to keep Hystrix enabled.
I have seen there is a new module that improves Hystrix - Feign (feign-hystrix module) in this regard in this post:
Does Spring Cloud Feign client call execute inside hystrix command?
However, I don't see how to properly do the setup using feign-hystrix and I was not able to find an example. Please, could you help with this or provide an example using feign-hystrix?
Thanks so much!
I am not exactly sure if I understood you correctly but the following worked for me.
See https://jfconavarrete.wordpress.com/2014/09/15/make-spring-security-context-available-inside-a-hystrix-command/
Basically the tutorial shows how to setup / augment hystrix with an additional "plugin" so the security context is made available inside hystrix wrapped calls via a threadlocal variable
With this setup all you need to do is define a feign request interceptor like so:
#Bean
public RequestInterceptor requestTokenBearerInterceptor() {
return new RequestInterceptor() {
#Override
public void apply(RequestTemplate requestTemplate) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
requestTemplate.header("Authorization", "Bearer " + details.getTokenValue());
}
};
}
With this setup the token contained in the request is made available to the feign request interceptor so you can set the Authorization header on the feign request with the token from your authenticated user.
Also note that with this approach you can keep your SessionManagementStrategy "STATELESS" as no data has to be "stored" on the server side
USE THIS CODE AND COMMENT RESTEMPLATE config when you are using as ribbon client instead of that here we will use oauth2restTemplate
#EnableOAuth2Client
#Configuration
public class OAuthClientConfig {
#Value("${config.oauth2.accessTokenUri}")
private String tokenUri;
#Value("${app.client.id}")
private String clientId;
#Value("${app.client.secret}")
private String clientSecret;
#Bean
protected OAuth2ProtectedResourceDetails resource() {
ResourceOwnerPasswordResourceDetails resource;
resource = new ResourceOwnerPasswordResourceDetails();
List<String> scopes = new ArrayList<String>(2);
scopes.add("write");
scopes.add("read");
resource.setAccessTokenUri(tokenUri);
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setGrantType("password");
resource.setScope(scopes);
return resource;
}
#Bean
public OAuth2ClientContext oauth2ClientContext() {
DefaultOAuth2ClientContext defaultOAuth2ClientContext = new DefaultOAuth2ClientContext();
return defaultOAuth2ClientContext;
}
#Bean
#Primary
#LoadBalanced
public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
OAuth2ClientContext oauth2ClientContext) {
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(oAuth2ProtectedResourceDetails, oauth2ClientContext);
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
restTemplate.setRequestFactory(factory);
return restTemplate;
}
#Bean
public OAuth2FeignRequestInterceptor aauthRequestInterceptor(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
OAuth2ClientContext oauth2ClientContext)
{
OAuth2FeignRequestInterceptor auth2FeignRequestInterceptor=new OAuth2FeignRequestInterceptor(oauth2ClientContext, oAuth2ProtectedResourceDetails);
return auth2FeignRequestInterceptor;
}

Resources