Spring WS not accepting basic auth credentials - spring-boot

I've been struggling with this one for a while and can't figure out what I'm doing wrong.
I'm supposed to add basic authentication to my SOAP web service in Spring. I made the security config pretty simple (maybe too simple) so it concentrates on basic auth only (see below).
When I'm accessing the base URL from the browser, the authentication seems to be working, it asks for the credentials and if I provide them correctly, it accepts them.
However, when I want to send the SOAP request that contains the basic auth header to my web service endpoint, Spring Security sends back 401 to me. I tried sending the request with SOAPUI, Postman and from Windows Powershell via Invoke-WebRequest, and the result is the same whereas if I catch the request with Wireshark, the right header is there.
I'm using Spring Boot 2.1.8 for this project (same version with Spring Web Services and Security).
The security config class:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("foo")
.password(passwordEncoder().encode("bar"))
.roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and().httpBasic();
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
To my understanding, there is nothing specific I have to add to the web service config itself so all related basic auth settings can be done in the security config class. Or am I wrong?
Appreciate your help.
Update
Here is the request/response pair:
REQUEST:
POST /foo/endpoint/ HTTP/1.1
Accept-Encoding: gzip,deflate
Content-Type: text/xml;charset=UTF-8
SOAPAction: "http://foo.bar"
Authorization: Basic Zm9vOmJhcg==
Content-Length: 9688
Host: localhost:1502
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.1 (java 1.5)
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:foo="http://foobar.com/">
<soapenv:Header/>
<soapenv:Body>
// body omitted
</soapenv:Body>
</soapenv:Envelope>
RESPONSE
HTTP/1.1 401
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
WWW-Authenticate: Basic realm="Realm"
Content-Length: 0
Date: Thu, 13 Aug 2020 14:22:29 GMT

configure(HttpSecurity http) needs some modifications to enable the http basic authentication in your code.
authorizeRequests() is used for authorization purposes. Once after a user successfully logged in, what all the resources(endpoints) should be accessible to logged-in user, is defined by authorizeRequests(). I also recommend, while authorizing the endpoints, you better use the antMatchers. For Example: I have modified the above code as
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("foo")
.password(passwordEncoder().encode("bar"))
.roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and()
.authorizeRequests()
.antMatchers("/simple/**").hasRole("USER");
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
I hope this would work for you. Try & let me know.
NOTE: In Spring boot, just by adding the spring-boot-starter-security dependency, security gets enabled, without any configurations. So now, you trying to re-configure the basic auto-configuration. By extending, WebSecurityConfigurerAdapter class. That's correct.
POSTMAN: use authorization header

Related

SecurityFilterChain .anyRequest().permitAll() doesn't allow POST requests

Using Spring Boot 2.7 (Spring Security 5.7.1) and trying to configure an API as a resource server and OAuth2 client I find a behavior I don't get to understand:
#EnableWebSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/swagger-ui/**", "/api-docs/**").permitAll()
.anyRequest().permitAll())
// register OAuth2 resource server
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
// register OAuth2 client
.oauth2Client(withDefaults());
return http.build();
}
}
Checking the logs, all this filters apply
o.s.s.web.DefaultSecurityFilterChain : Will secure any request with
org.springframework.security.web.session.DisableEncodeUrlFilter#320ca97c,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#3c592c0c,
org.springframework.security.web.context.SecurityContextPersistenceFilter#2b33e616,
org.springframework.security.web.header.HeaderWriterFilter#2e9bff08,
org.springframework.security.web.csrf.CsrfFilter#7926d092,
org.springframework.security.web.authentication.logout.LogoutFilter#37227aa7,
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter#6f18445b,
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter#42af2977,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter#79e3f444, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#1252d480,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter#3979c6e8,
org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter#19faa9dc,
org.springframework.security.web.session.SessionManagementFilter#7d3b4646,
org.springframework.security.web.access.ExceptionTranslationFilter#6cb2d5ea,
So far, this configuration works as expected in the other APIs I'm protecting. However, in this particular one, AND not having protected any endpoint I see that:
I can access any GET endpoint but any POST endpoint returns a 403 FORBIDDEN. However, I can access them if I add a token to the request EVEN if its an expired token
This alone I can't understand as .anyRequest().permitAll() should unprotect anything, if I'm not wrong.
If I comment out the line configuring the oauth2 ResourceServer
`// .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)`
this filter dissapears
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter
And can't use POST endpoints anymore, even with the expired token
Logically, I want the API to be oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) so
How it is I can't access POST endpoints when using .anyRequest().permitAll())?
DISCLAIMER: I know it makes no sense to declare the API as resource server if all endpoints must be public. Endpoints will be accessed by Discord callbacks and I have to figure out if I can protect them with OAuth
EDIT:
server.servlet.context-path = /api
Controller
#RestController
#RequestMapping("/slack")
public class SlackBotController {
#PostMapping("/test")
public String test(#RequestBody String a) {
return a;
}
#GetMapping("/test")
public String testGet() {
return "OK";
}
}
Request
GET/POST http://localhost:8081/api/slack/test
Request headers
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Postman-Token: f20ba7a6-26e5-47c4-a827-0596afec27b8
Host: localhost:8081
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 3
Cookie: JSESSIONID=D1C2B2668DC130C63DDE03F30574ED5F; JSESSIONID=823D79956CFBF14F3C77C96E29F4131C
Response headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 03 Jun 2022 12:03:00 GMT
Keep-Alive: timeout=60
Connection: keep-alive
The reason why the POST endpoint or the POST returns a 403 FORBIDDEN because the CSRF protection is enabled by default in spring.
That means that every modifying request (POST, PUT, DELETE, PATCH) requires a CSRF token. Otherwise the request gets denied to prevent CSRF attacks.
To disable the CSRF protection:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable();
}
The problem here is the CSRF protection. When you use http.oauth2ResourceServer() Spring Security configures CSRF to ignore requests that contains the header Authorization: Bearer whatever, note that it has to contain the Bearer prefix.
In the request sample that you shared, you are not including the Authorization header.
Take a look at the code in Spring Security.

Spring Boot Security: Why Strict-Transport-Security header is not including preload; when preload set to true

I have a spring boot application where i have added WebSecurityConfigurerAdapter.
I want to have the response header strict-transport-security: max-age=16000000; includeSubDomains; preload;
I have added the below code to the SecurityConfig as:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.headers(headers ->
headers.httpStrictTransportSecurity(hstsConfig -> {
hstsConfig.preload(true);
hstsConfig.includeSubDomains(true);
hstsConfig.maxAgeInSeconds(31536000);
})
.contentSecurityPolicy(contentSecurityPolicy ->
contentSecurityPolicy
.policyDirectives("script-src 'self'")
)
.addHeaderWriter(new StaticHeadersWriter("X-My-Custom-Header","myvalue"))
);
http.csrf().disable();
}
The response header is not adding the "preload;" instead it looks like:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Can someone please help me to understand what am I missing here?
Spring Security Config: v5.4.6
Thanks

Secure specific endpoint by role

I'm trying to build REST API with Spring Boot, secured by Spring Security. Here I need to provide /users endpoint which will be available only to users with ADMIN role.
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_ADMIN");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
http.authorizeRequests()
.antMatchers("/users").hasRole("ADMIN")
.antMatchers("/products").permitAll()
;
}
}
I'm using TestingAuthenticationToken with ROLE_ADMIN, so I expect that /users endpoint will be available in this configuration.
Request:
GET /users HTTP/1.1
Host: localhost:5000
Accept: application/json
Content-Type: application/json
Cache-Control: no-cache
Response:
"timestamp": "2020-09-01T17:28:27.628+00:00",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/users"
}
The SecurityContext with its Authentication is retrieved during each request in the SecurityContextPersistenceFilter. Your SecurityContext in the SecurityConfig is hence simply overwritten (with auth == null). No authentication means no role hence 403 - forbidden.
For more see here.

Duplicate Cache-Control header in Spring Boot application

We have a Spring Boot application that runs in Openshift where we configure the Cache-Control header like this:
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl().disable().addHeaderWriter((httpServletRequest, httpServletResponse) -> {
httpServletResponse.setHeader(HttpHeaders.CACHE_CONTROL, "public, max-age=86400");
});
}
}
In the HTTP responses there are two Cache-Control headers:
$ curl --header https://<our-url> --head
HTTP/1.1 200 Connection established
HTTP/1.1 200
...
Cache-Control: public, max-age=86400
...
Cache-control: private
We expect the first header, and we have no idea where the second header comes from. (Note the lowercase c in the name of the second header.)
Any ideas where the second header comes from and how we can get rid of it?
I found the answer: The Cache-control header is added by HAProxy. HAProxy uses this header and a cookie to create sticky sessions (i.e. to make sure that requests from the same client are handled by the same pod).
See this question for details.
In short, you can disable this behaviour by
oc annotate route <myroute> haproxy.router.openshift.io/disable_cookies='true'

Spring Security : Falls back to formLogin on authentication error

I tried to implement a SecurityConfig similar to https://stackoverflow.com/a/33608459 and https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#multiple-httpsecurity
I want my API (/rest/**) to be secured by HttpBasic, and other requests via FormLogin.
This works well... as long as I provide the correct credentials to HttpBasic.
If I provide correct credentials - it response with normal answer.
If I provide no credentials - it responds with a 401 Unauthorized - perfect!
If I provide wrong credentials - it responds with a 302 Found with Location: /login
The last part is what I don't want - I also want a 401 Unauthorized on wrong credentials.
Example Requests:
http http://localhost:8081/rest/
HTTP/1.1 401 WWW-Authenticate: Basic realm="My Realm"
http -a correct:password http://localhost:8081/rest/some/api/
HTTP/1.1 200
http -a wrong:password http://localhost:8081/rest/some/api/
HTTP/1.1 302 Location: http://hive.local:8081/login WWW-Authenticate: Basic realm="My Realm"
Java configuration:
#Configuration
#Order(1)
public static class RestSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthorizationProperties properties;
#Override protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/rest/**")
.authorizeRequests()
.anyRequest().hasRole("API").and()
.httpBasic()
.realmName(properties.getRealm()).and()
.formLogin().disable()
.csrf().disable();
// #formatter:on
}
}
#Configuration
#Order(2)
public static class FrontendSecurityConfig extends WebSecurityConfigurerAdapter {
#Override public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/app/**", "/webjars/**", "/static/**", "/js/**");
}
#Override protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.authorizeRequests()
.anyRequest().hasAnyRole("USER").and()
.formLogin();
// #formatter:on
}
}
I was able to bring some light into this.
The redirect to form login after a failed basic auth request is cause by the dispatcher servlet trying to redirect to the URL /error after failing to validate the credentials.
To get the appropriate error response you need to add /error to the antMatchers that are ignored in your web security config.

Resources