I have a Spring Boot web application, where most endpoints require authentication. However, a few mappings shall allow anonymous access; they are exceptions from the general rule.
I cannot get this to work for POST calls, they are always getting 403.
The security configuration...
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().regexMatchers("/", "/posthello").anonymous()
.and()
.authorizeRequests().anyRequest().authenticated();
}
}
The controller...
#RestController
public class HelloController {
// This returns HTTP 200 and body on anonymous calls
#GetMapping("/")
public String helloWorld() {
return "Hello World!";
}
// This demands authentication, as expected
#GetMapping("/gethello")
public String getHelloWorld(String body) {
return "You got: Hello, World!";
}
// This always returns HTTP 403 on anonymous calls,
// even though it is supposed to be excepted
#PostMapping("/posthello")
public String postHelloWorld(#RequestBody String body) {
return "You posted: " + body;
}
}
Patel Romil is correct that the 403 is caused by CSRF and that disabling CSRF would disable that protection. He's also wise to warn against doing that in a production application.
An alternative to disabling CSRF for the entire site is specifying an allowlist of endpoints, like so:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.ignoringAntMatchers("/posthello")
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/posthello").anonymous()
.anyRequest().authenticated();
}
}
That said, the real solution is likely to configure the application to use CSRF tokens. Without CSRF protection, arbitrary third-party web applications can invoke POST /posthello.
You can mention the endpoints like this, for which authenication is not required.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/", "/error", "/user/signup", "/user/signin", "/swagger-ui.html")
.permitAll().anyRequest().authenticated().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Forbidden errors might occur due to csrf. You can try disabling the csrf in your configure method as below.
http
.csrf().disable()
// remaining configurations
Alternatively, you can add the following hidden field in your form which is sending the POST request. (It is for thymeleaf. You can get this as per your view)
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
You have to disable the CSRF Tokens using csrf().disable(). I recommended you to use the CSRF Tokens in production. Along with that, you may allow the anonymous() access for the specific methods by using the HttpMethod.method in antMatchers
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/posthello").anonymous()
.anyRequest().authenticated();
}
}
OR
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/posthello").anonymous()
.antMatchers("**/**").authenticated();
}
}
To enable the CSRF in production I have added the more details here
Answer 1, Answer 2
Related
EDIT:
I further drilled down the problem and turns out issue persists even with single configuration. If I use single configuration and keep
http.antMatcher("/api/test/**")
urls don't get secured.
Removing the antMatcher and antMatchers immediately secures the url.
i.e if I use:
http.httpBasic()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
then only spring security is securing url. Why isn't antMatcher functioning?
(Updated the title to include actual issue.)
Original Post:
I have referred following stackoverflow questions:
Spring REST security - Secure different URLs differently
Using multiple WebSecurityConfigurerAdapter with different AuthenticationProviders (basic auth for API and LDAP for web app)
and spring security doc:
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#multiple-httpsecurity
But I am not able to configure multiple http security elements.
When I follow the official spring doc, it works in my case only becuase of the fact that the second http security element is a catch-all, but as soon as I add a specific url, all the urls can be accessed without any authentication.
Here's my code:
#EnableWebSecurity
#Configuration
public class SecurityConfig {
#Bean
public UserDetailsService userDetailsService() throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user").password("userPass").roles("USER").build());
manager.createUser(User.withUsername("admin").password("adminPass").roles("ADMIN").build());
return manager;
}
#Configuration
#Order(1)
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Override
public void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("user").password("user").roles("USER");
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
}
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/v1/**")
.authorizeRequests()
.antMatchers("/api/v1/**").authenticated()
.and()
.httpBasic();
}
}
#Configuration
#Order(2)
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Override
public void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("user1").password("user").roles("USER");
auth.inMemoryAuthentication().withUser("admin1").password("admin").roles("ADMIN");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/test/**")
.authorizeRequests()
.antMatchers("/api/test/**").authenticated()
.and()
.formLogin();
}
}
}
Now any url can be accessed. If I remove antMatcher from second configuration, all the urls become secured.
The pattern must not contain the context path, see AntPathRequestMatcher:
Matcher which compares a pre-defined ant-style pattern against the URL ( servletPath + pathInfo) of an HttpServletRequest.
and HttpServletRequest.html#getServletPath:
Returns the part of this request's URL that calls the servlet. This path starts with a "/" character and includes either the servlet name or a path to the servlet, but does not include any extra path information or a query string. Same as the value of the CGI variable SCRIPT_NAME.
and HttpServletRequest.html#getContextPath:
Returns the portion of the request URI that indicates the context of the request. The context path always comes first in a request URI. The path starts with a "/" character but does not end with a "/" character. For servlets in the default (root) context, this method returns "". The container does not decode this string.
Your modified and simplified code:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/test/**")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin();
}
I have this WebSecurityConfigurerAdapter config:
#EnableWebSecurity
public class HttpSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.authorizeRequests()
.mvcMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt()
;
}
}
When I make a request to auth, I get a 401, until I pass some authorisation - which isn't appropriate for this endopint.
I would assume this has something to do with .anyRequest().authenticated(). I had read before that this shouldn't affect the permitAll()s - have I misconfigured something?
Your request is probably being rejected because you didn't provide a CSRF token. By default, Spring Security enables it for every POST request and you need to explicitly disable it.
#EnableWebSecurity
public class HttpSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf().disable()
.authorizeRequests()
.mvcMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
}
}
You can add the following property to your application.yml file so you can be able to see why your request is being rejected if CSRF is not the case:
logging:
level:
org.springframework.security: TRACE
If u are using jwt filter,it won't work even if you added permitAll() .if you remove the filter, it will work fine.
I'm trying to add web security in spring but I don't want the filter to apply to certain things. How is that done in java?
Overall, what I want to do is this:
/ and /login should not show a HTTP Basic authentication prompt to login, while everything else should go through the filter and pop up a login prompt window.
Through various example I found through spring I was able to come up with this as for a start but it obviously doesn't work:
#Configuration
#EnableWebMvcSecurity
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().antMatcher("/").authorizeRequests().anyRequest().permitAll();
http.csrf().disable().antMatcher("/**").authorizeRequests().anyRequest().hasRole("ADMIN").and().httpBasic();
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("admin123").roles("ADMIN")
.and()
.withUser("user").password("user123").roles("USER");
}
}
Rewrite your configure(HttpSecurity http) method like the following:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/", "/login").permitAll()
.anyRequest().hasRole("ADMIN")
.and()
.csrf()
.disable();
}
"/" and "/login" SHOULD NOT show a httpbasic authentication prompt to
login, while everything else SHOULD go through the filter and pop up a
login prompt window.
If you seriously planning to use HTTP Basic, I guess you wouldn't need a separate /login handler, since browser-based clients can use the default browser based pop up and other clients can send HTTP Basic requests through Authorization header.
I am trying to give role based authorization for resources. It works with out roles if I do it like
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/hello"),
new AntPathRequestMatcher("/user")
))
.authorizeRequests()
.anyRequest().access("#oauth2.hasScope('read')");
}
#Override
public void configure(ResourceServerSecurityConfigurer resources)
throws Exception {
resources.resourceId("openid");
}
}
If I use below method it won't work for test resources.
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.requestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/hello"),
new AntPathRequestMatcher("/user")
))
.authorizeRequests()
.antMatchers("/test").hasRole("ADMIN")
.anyRequest().access("#oauth2.hasScope('read')");
}
It completely ignores token based authorization. How can I implement this? Another issue I am getting is if I remove requestMatcher block, Oauth client can not get the authorization code, after submitting user credentials to login form it reloads login page again. But with the previous block of code it works fine. What I am doing wrong here?
Here is my security configuration class
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/img/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").permitAll()
.defaultSuccessUrl("/hello")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout");
}
}
When you use roles in spring you have to use prefix ROLE (for example ROLE_ADMIN) to make it work with default settings.
I want to enable the use of "ROLE_ANONYMOUS" to allow anonymous access to some urls in my app. And I used the below configuration.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestCache()
.requestCache(new NullRequestCache()).and()
.anonymous().authorities("ROLE_ANONYMOUS").and()
.exceptionHandling().and()
.servletApi().and()
.headers().cacheControl().and()
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/profile/image").permitAll()
.antMatchers("/favicon.ico").permitAll()
.antMatchers("/resources/**").permitAll()
//.antMatchers(HttpMethod.GET, "/login/**").permitAll()
//.antMatchers(HttpMethod.GET, "/location/**").permitAll()
.anyRequest().authenticated()/*.and()
.apply(new SpringSocialConfigurer())*/;
// custom Token based authentication based on the header previously given to the client
//.addFilterBefore(new StatelessAuthenticationFilter(tokenAuthenticationService), UsernamePasswordAuthenticationFilter.class);
}
My controller looks like:
#RestController
#RequestMapping(value="/login", produces="application/json")
public class LoginController {
#Secured( value={"ROLE_ANONYMOUS"})
#RequestMapping(method=RequestMethod.GET)
public String get(){
return "hello";
}
}
But when I try to hit "/login" I get 403 access denied error.
Please help me how I can enable annotation based anonymous access.
This should solve your issue.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.formLogin().loginPage("/login").permitAll()
...
But if you prefer not to use permitAll but to stick to anonymous roled user (it would be the same effect on both situation but yet if that's wht you prefer) then try this in the controller.
#Secured("ROLE_ANONYMOUS")
#RequestMapping(method=RequestMethod.GET)
public String get(){
...
As Faraj Farook wrote, you have to permit access to your login page URL. You commented the relevant line out:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.anonymous()
.authorities("ROLE_ANONYMOUS")
.and()
.headers()
.cacheControl()
.and()
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/profile/image").permitAll()
.antMatchers("/favicon.ico").permitAll()
.antMatchers("/resources/**").permitAll()
.antMatchers(HttpMethod.GET, "/login/**").permitAll()
.anyRequest().authenticated()
}
But if you prefer not to use permitAll() you could use hasAuthority("ROLE_ANONYMOUS"). In this case you don't need to annotate your method with
#Secured( value={"ROLE_ANONYMOUS"}).