Spring Security 6 Request Matchers and URLs containing IDs - spring-boot

I'm trying to upgrade to spring boot 3 and spring security 6.
I'm running into an issue where previously, mvcMatchers would match against these URLs:
String[] companiesEndpoints = {"/companies", "/companies/*"};
String[] ideationEndpoint = {"/ideation", "/ideation/*"};
String[] assessmentsEndpoints = {"/assessment", "/assessment/*", "/assessment/*/value-rating", "/assessment/*/viability-rating", "/assessment/*/customer-rating"};
String[] teamsEndpoints = {"/teams", "/teams/*"};
String[] userEndpoints = {"/users", "/users/*"};
String[] projectEndpoints = {"/project", "/project/*", "/project-and-assessment"};
String[] workspaceEndpoints = {"/workspace", "/workspace/*"};
String[] tagEndpoints = {"/tag", "/tag/*"};
As part of the move to spring security 6, mvcMatchers are now replaced with requestMatchers - I thought I could drop in the new matches and things would keep working, but now the listed URLs before only allow some of the requests. Here are some examples:
.mvcMatchers(assessmentsEndpoints).authenticated() use to match
"/assessment/2900b695-d344-4bec-b25d-524f6b22a93a/customer-rating".
.requestMatchers(assessmentsEndpoints).authenticated() does not match so the API returns a 403.
This makes me think that requestMatcher is not a drop in replacement for mvcMatcher, but I'm not sure how I should be structuring this to make requestMatcher allow these requests.
I have many requests with path parameters like "/assessment/{assessmentId}/value-rating*". How should I structure my String[] endpoints to allow such URLs?
For reference, here is the full SecurityConfig class that contains the relevant code.
#Configuration
public class SecurityConfig {
#Value(value = "${auth0.audience}")
private String apiAudience;
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
#Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(apiAudience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(
"http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList(
"x-requested-with",
"content-type",
"Accept",
"Authorization",
"sentry-trace",
"baggage"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
String[] companiesEndpoints = {"/companies", "/companies/*"};
String[] ideationEndpoint = {"/ideation", "/ideation/*"};
String[] assessmentsEndpoints = {"/assessment", "/assessment/*", "/assessment/*/value-rating", "/assessment/*/viability-rating", "/assessment/*/customer-rating"};
String[] teamsEndpoints = {"/teams", "/teams/*"};
String[] userEndpoints = {"/users", "/users/*"};
String[] projectEndpoints = {"/project", "/project/*", "/project-and-assessment"};
String[] workspaceEndpoints = {"/workspace", "/workspace/*"};
String[] tagEndpoints = {"/tag", "/tag/*"};
http.authorizeHttpRequests((authorize) -> {
try {
authorize
.requestMatchers(companiesEndpoints).authenticated()
.requestMatchers(ideationEndpoint).authenticated()
.requestMatchers(assessmentsEndpoints).authenticated()
.requestMatchers(teamsEndpoints).authenticated()
.requestMatchers(userEndpoints).authenticated()
.requestMatchers(projectEndpoints).authenticated()
.requestMatchers(workspaceEndpoints).authenticated()
.requestMatchers(tagEndpoints).authenticated()
.requestMatchers(EndpointRequest.to("info")).hasAuthority("SCOPE_read:status")
.requestMatchers(EndpointRequest.to("health")).permitAll()
.and()
.oauth2ResourceServer((oauth2ResourceServer) ->
oauth2ResourceServer.jwt(jwt -> jwt.decoder(jwtDecoder())));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
// Disable X-Frames on same origin to enable access to H2 in memory db console
// https://stackoverflow.com/questions/26220083/h2-database-console-spring-boot-load-denied-by-x-frame-options
http.headers().frameOptions().sameOrigin();
return http.build();
}
}

Adding debug logging as dur suggested revealed the issue.
There was no issue, spring was actually authorising these requests.
Poking the API with postman responded with 200s across the board, so the requestMatchers did just fine.
Integration tests failed though! Some quick investigation and debugging revealed a change in Spring Security 6 that I wasn't aware of - the need to add csrf protection to controller tests.
Here is a failing unit test that I was using to diagnose and debug the issue:
#Test
#Transactional
void updateValueRating() throws Exception {
ValueRating newRating = ValueRatingBuilder.aValueRating()
.withEarnableRevenue(50)
.withRevenueAtRisk(50)
.build();
mockMvc.perform(put("/assessment/2900b695-d344-4bec-b25d-524f6b22a93a/value-rating")
.content(mapper.writeValueAsString(newRating))
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.earnableRevenue", is(50)))
.andExpect(jsonPath("$.revenueAtRisk", is(50)))
.andExpect(jsonPath("$.overallValueRating", is(1.5)));
}
Adding the static print call .andDo(print())) revealed a message from Spring Security's CSRF filter: Invalid CSRF token found, which then responded with a 403 in the unit test, where poking with HTTP worked fine.
A quick google yielded https://docs.spring.io/spring-security/reference/servlet/test/mockmvc/csrf.html - adding .with(csrf()) to the test like so, fixed the issue.
mockMvc.perform(put("/assessment/2900b695-d344-4bec-b25d-524f6b22a93a/value-rating")
.content(mapper.writeValueAsString(newRating))
.contentType(MediaType.APPLICATION_JSON)
.with(csrf()))
I didn't realise that this changed between Spring Security 5 and 6 so it caught be by surprise. In case anyone hits a similar issue I'm posting this answer.

Related

Customize Spring OAuth2TokenEndpointFilter

I need to customize the Authentication Success Handler method for the Spring Authorization Server OAuth2TokenEndpointFilter to simulate a OAuth2 provider with limited capabilities.
I've tried the following:
#Bean
public OAuth2TokenEndpointFilter oauth2TokenEndpointFilter(AuthenticationManager authenticationManager) {
OAuth2TokenEndpointFilter filter = new OAuth2TokenEndpointFilter(authenticationManager);
// this is ugly but seems to be the only way to customize the token format.
filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
additionalParameters.put("expires_in", null);
additionalParameters.put("user_id", null);
final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
new OAuth2AccessTokenResponseHttpMessageConverter();
OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.refreshToken(refreshToken.getTokenValue())
.additionalParameters(additionalParameters);
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
});
return filter;
}
Unfortunately, the token still comes back with the default fields in the token.
Can anyone tell me what I'm doing wrong?

aws elasticsearch signed delete request returns 403 forbidden (using apache httpclient)

I'm getting an http 403 forbidden error trying to delete an aws elasticsearch index via the java Jest(v6.3) elasticsearch client (which delegates the http calls to apache httpclient(v4.5.2) I know my permissions are setup correctly in AWS as I'm able to successfully use postman(with the help of AWS Signature authorization helper). however, with apache httpclient, when I issue the DELETE /{myIndexName} I receive the following error:
The request signature we calculated does not match the signature you provided.
Check your AWS Secret Access Key and signing method.
Consult the service documentation for details.
I'm signing the aws request by configuring the apache httpclient with an interceptor that signs the request.(The code below is for a Spring Framework #Configuration class that wires up the java Jest client and underlying apache httpclient) but I imagine if I used apache httpclient directly I'd experience the same issue.
#Configuration
public class ElasticSearchConfiguration {
#Autowired
private CredentialsProviderFactoryBean awsCredentialsProvider;
#Bean
public JestClient awsJestClient(#Value("${elasticsearch.url}") String connectionUrl) throws Exception {
com.amazonaws.auth.AWSCredentialsProvider provider = awsCredentialsProvider.getObject();
final com.google.common.base.Supplier<LocalDateTime> clock = () -> LocalDateTime.now(ZoneOffset.UTC);
final vc.inreach.aws.request.AWSSigner awsSigner = new vc.inreach.aws.request.AWSSigner(provider, "us-east-1", "es", clock);
final vc.inreach.aws.request.AWSSigningRequestInterceptor requestInterceptor = new vc.inreach.aws.request.AWSSigningRequestInterceptor(awsSigner);
final JestClientFactory factory = new JestClientFactory() {
#Override
protected HttpClientBuilder configureHttpClient(HttpClientBuilder builder) {
builder.addInterceptorLast(requestInterceptor);
return builder;
}
#Override
protected HttpAsyncClientBuilder configureHttpClient(HttpAsyncClientBuilder builder) {
builder.addInterceptorLast(requestInterceptor);
return builder;
}
};
factory.setHttpClientConfig(new HttpClientConfig
.Builder(connectionUrl)
.connTimeout(60000)
.multiThreaded(true)
.build());
return factory.getObject();
}
}
Since it's working with postman it points to the a signing error but I'm at a loss to where the discrepancy is occurring. The configuration above works for all apache httpclient requests besides http DELETE requests.
After a bunch of research I found some clues that pointed to the possibility that the presence of the Content-Length (length=0) in request issued to aws were causing the signature mismatch. I'm guessing that the signature done via the client interceptor was not factoring in the Content-Length header but since we were sending the Content-Length header to the aws server it was factoring it in and thus causing the signature mismatch. I believe this to be the case because I added an additional interceptor(before the AWS signing interceptor) that explicitly removes the Content-Length header for DELETE requests and the request goes through successfully. (i.e. I'm able to delete the index). Updated code below:
#Configuration
public class ElasticSearchConfiguration {
private static final Logger log = LoggerFactory.getLogger(ElasticSearchConfiguration.class);
#Autowired
private CredentialsProviderFactoryBean awsCredentialsProvider;
#Bean
public JestClient awsJestClient(#Value("${elasticsearch.url}") String connectionUrl) throws Exception {
com.amazonaws.auth.AWSCredentialsProvider provider = awsCredentialsProvider.getObject();
final com.google.common.base.Supplier<LocalDateTime> clock = () -> LocalDateTime.now(ZoneOffset.UTC);
final vc.inreach.aws.request.AWSSigner awsSigner = new vc.inreach.aws.request.AWSSigner(provider, "us-east-1", "es", clock);
final vc.inreach.aws.request.AWSSigningRequestInterceptor requestInterceptor = new vc.inreach.aws.request.AWSSigningRequestInterceptor(awsSigner);
final HttpRequestInterceptor removeDeleteMethodContentLengthHeaderRequestInterceptor = (request, context) -> {
if(request.getRequestLine().getMethod().equals("DELETE")) {
log.warn("intercepted aws es DELETE request, will remove 'Content-Length' header as it's presence invalidates the signature check on AWS' end");
request.removeHeaders("Content-Length");
}
};
final JestClientFactory factory = new JestClientFactory() {
#Override
protected HttpClientBuilder configureHttpClient(HttpClientBuilder builder) {
builder.addInterceptorLast(removeDeleteMethodContentLengthHeaderRequestInterceptor);
builder.addInterceptorLast(requestInterceptor);
return builder;
}
#Override
protected HttpAsyncClientBuilder configureHttpClient(HttpAsyncClientBuilder builder) {
builder.addInterceptorLast(removeDeleteMethodContentLengthHeaderRequestInterceptor);
builder.addInterceptorLast(requestInterceptor);
return builder;
}
};
factory.setHttpClientConfig(new HttpClientConfig
.Builder(connectionUrl)
.connTimeout(60000)
.multiThreaded(true)
.build());
return factory.getObject();
}
}

Streaming upload via #Bean-provided RestTemplateBuilder buffers full file

I'm building a reverse-proxy for uploading large files (multiple gigabytes), and therefore want to use a streaming model that does not buffer entire files. Large buffers would introduce latency and, more importantly, they could result in out-of-memory errors.
My client class contains
#Autowired private RestTemplate restTemplate;
#Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
int REST_TEMPLATE_MODE = 1; // 1=streams, 2=streams, 3=buffers
return
REST_TEMPLATE_MODE == 1 ? new RestTemplate() :
REST_TEMPLATE_MODE == 2 ? (new RestTemplateBuilder()).build() :
REST_TEMPLATE_MODE == 3 ? restTemplateBuilder.build() : null;
}
and
public void upload_via_streaming(InputStream inputStream, String originalname) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setBufferRequestBody(false);
restTemplate.setRequestFactory(requestFactory);
InputStreamResource inputStreamResource = new InputStreamResource(inputStream) {
#Override public String getFilename() { return originalname; }
#Override public long contentLength() { return -1; }
};
MultiValueMap<String, Object> body = new LinkedMultiValueMap<String, Object>();
body.add("myfile", inputStreamResource);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body,headers);
String response = restTemplate.postForObject(UPLOAD_URL, requestEntity, String.class);
System.out.println("response: "+response);
}
This is working, but notice my REST_TEMPLATE_MODE value controls whether or not it meets my streaming requirement.
Question: Why does REST_TEMPLATE_MODE == 3 result in full-file buffering?
References:
How to forward large files with RestTemplate?
How to send Multipart form data with restTemplate Spring-mvc
Spring - How to stream large multipart file uploads to database without storing on local file system -- establishing the InputStream
How to autowire RestTemplate using annotations
Design notes and usage caveats, also: restTemplate does not support streaming downloads
In short, the instance of RestTemplateBuilder provided as an #Bean by Spring Boot includes an interceptor (filter) associated with actuator/metrics -- and the interceptor interface requires buffering of the request body into a simple byte[].
If you instantiate your own RestTemplateBuilder or RestTemplate from scratch, it won't include this by default.
I seem to be the only person visiting this post, but just in case it helps someone before I get around to posting a complete solution, I've found a big clue:
restTemplate.getInterceptors().forEach(item->System.out.println(item));
displays...
org.SF.boot.actuate.metrics.web.client.MetricsClientHttpRequestInterceptor
If I clear the interceptor list via setInterceptors, it solves the problem. Furthermore, I found that any interceptor, even if it only performs a NOP, will introduce full-file buffering.
public class SimpleClientHttpRequestFactory { ...
I have explicitly set bufferRequestBody = false, but apparently this code is bypassed if interceptors are used. This would have been nice to know earlier...
#Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
prepareConnection(connection, httpMethod.name());
if (this.bufferRequestBody) {
return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
}
else {
return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
}
}
public abstract class InterceptingHttpAccessor extends HttpAccessor { ...
This shows that the InterceptingClientHttpRequestFactory is used if the list of interceptors is not empty.
/**
* Overridden to expose an {#link InterceptingClientHttpRequestFactory}
* if necessary.
* #see #getInterceptors()
*/
#Override
public ClientHttpRequestFactory getRequestFactory() {
List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
if (!CollectionUtils.isEmpty(interceptors)) {
ClientHttpRequestFactory factory = this.interceptingRequestFactory;
if (factory == null) {
factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
this.interceptingRequestFactory = factory;
}
return factory;
}
else {
return super.getRequestFactory();
}
}
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest { ...
The interfaces make it clear that using InterceptingClientHttpRequest requires buffering body to a byte[]. There is not an option to use a streaming interface.
#Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {

Spring SSO with facebook filter and JWT

here is my problem.
I got a first authentication with mail and JWT with Spring boot 1.5.3.
=> works perfectly
Then i made a SSO filter to allow facebook tokens
The thing is, on first authentication it's ok. My server get the Token, then check with fb that says ok then it says ok to my client.
After that if i don't encode my token with my JWT token enhancer, my server says that it is not able to decode it as JSON.
Just that i know, i would normally not have to encode myself as it should be done automatically after my chain filter if i say ok ??
This code works but i've done the jwt myself, is that possible i've missed something ????
public class MyOAuth2ClientAuthenticationProcessingFilter extends OAuth2ClientAuthenticationProcessingFilter {
public MyOAuth2ClientAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
log.info("[attemptAuthentication facebook]");
Authentication result = null;
try {
String token = request.getHeader("oauth_token");
oauth2ClientContext.setAccessToken(new DefaultOAuth2AccessToken(token));
result = super.attemptAuthentication(request, response);
if(result.isAuthenticated()) {
FacebookService facebookService = new BasicFacebookService(token);
User fbUser = facebookService.getUser();
if(fbUser == null) {
throw new IllegalArgumentException(" fb user cannot be null");
}
if(!userService.isLoginExists(fbUser.getId())) {
CreateSocialUserModel model = new CreateSocialUserModel(
token,
DateUtil.getNow(),
"facebook");
userService.createSocialUser(model, fbUser);
}
//--- Create custom JWT token from facebook token
UserInfoTokenServices tokenService = new UserInfoTokenServices(
"https://graph.facebook.com/me",
facebookProperties.getAppId());
OAuth2AccessToken enhancedToken = jwtTokenEnhancer.enhance(oauth2ClientContext.getAccessToken(),
tokenService.loadAuthentication(oauth2ClientContext.getAccessToken().getValue()));
TokenResponse tokenResponse = new TokenResponse(enhancedToken.getValue(),
enhancedToken.getTokenType(),
enhancedToken.getRefreshToken() != null ? enhancedToken.getRefreshToken().getValue() : "");
ObjectMapper mapper = new ObjectMapper();
String jsonTokenEnhancedJack = mapper.writeValueAsString(tokenResponse);
response.addHeader("Content-Type", "application/json");
response.getWriter().flush();
response.getWriter().print(jsonTokenEnhancedJack);
}
return result;
} catch (Exception e) {
log.info("error");
log.error("error", e);
e.printStackTrace();
} finally {
return result;
}
}
}
Thank you in advance
As asked by Son Goku just putting some code to help him
First you have to put the filter like this
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().permitAll()
.and().csrf().disable();
}
private Filter ssoFilter() {
OAuth2ClientAuthenticationProcessingFilter facebookFilter = new MyOAuth2ClientAuthenticationProcessingFilter("/user/social");
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(Oauth2facebook(), oauth2ClientContext);
facebookFilter.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(new UserInfoTokenServices(
"https://graph.facebook.com/me",
facebookProperties.getAppId()
));
return facebookFilter;
}
After that you can use the OAuth2ClientAuthenticationProcessingFilter as in first question.
Also, in first question i customized this method a lot, it works but i was surprised to not find this as easy as these library use to be.
Hope it helps you, i have struggled a bit too on this.
Maybe now, spring boot handle this much more easily.

Hystrix-javanica #fallbackMethod Last Cached Response

I'm looking to do something like the following:
#HystrixCommand(fallbackMethod = "returnLastGoodCachedCopy")
void performRequest(String someArg) {
// Make HTTP request using RestTemplate configured with EHCache
}
void returnLastGoodCachedCopy(String someArg) {
// Return the last successful response
}
For a little more background I am using Spring Boot, and setting up the RestTemplate like so to use EHCache:
#Bean
public RestTemplate restTemplate() {
return new RestTemplate(clientHttpRequestFactory());
}
#Bean
public CloseableHttpClient httpClient() {
CacheConfig cacheConfig = CacheConfig.custom()
.setMaxCacheEntries(this.httpClientProperties.getMaxCacheEntries())
.setMaxObjectSize(this.httpClientProperties.getMaxObjectSize())
.setHeuristicCachingEnabled(this.httpClientProperties.getHeuristicLifetimeEnabled())
.setHeuristicDefaultLifetime(this.httpClientProperties.getHeuristicLifetimeSeconds()).build();
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(this.httpClientProperties.getConnectTimeout())
.setSocketTimeout(this.httpClientProperties.getSocketTimeout()).build();
Ehcache httpEhcache = (Ehcache) this.ehCacheManager.getCache(httpClientProperties.getCacheName())
.getNativeCache();
EhcacheHttpCacheStorage ehcacheHttpCacheStorage = new EhcacheHttpCacheStorage(httpEhcache);
CloseableHttpClient cachingClient = CachingHttpClients.custom().setCacheConfig(cacheConfig)
.setHttpCacheStorage(ehcacheHttpCacheStorage).setDefaultRequestConfig(requestConfig).build();
return cachingClient;
}
private ClientHttpRequestFactory clientHttpRequestFactory() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient());
return factory;
}
So my one thought was to use the same request EHCache, but I'm not sure that's an appropriate solution considering that cache is based on cache-control headers, which I'd like to be separated from so I can return a valid response regardless if it is expired or not.
My other thought was to configure a separate EHCache cache, and store the responses myself, then I can access those more easily considering I set the format of the key and the value.
Before I go down this path I want to see if there is already anything built into Hystrix that handles this situation, or if there are any other recommended approaches.

Resources