Basic auth not working with POST endpoint

Viewed 1351

I have built a web server that I am trying to password protect. I am trying to set up basic auth using spring boot. This is my configuration file so far:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests()
         .antMatchers("/", "/v1/occupancy/*")
         .permitAll()
         .anyRequest()
         .authenticated()
         .and()
         .httpBasic();
   }
}

This works as expected and protects one of my GET endpoints, allowing me to authenticate.

However, for a POST endpoint, this does not work. The endpoint looks like this:

@RequestMapping(path = "/v1/admin/repository")
public class RepositoryOptionsController {

    private final EstablishmentOptionsRepositoryService establishmentOptionsRepositoryService;
    private final SubAreaOptionsRepositoryService subAreaOptionsRepositoryService;
@PostMapping("/establishment/options")
public ResponseEntity<String> postEstablishmentOption(@RequestBody OptionsRequestDto body) {

When I do

curl -X POST "http://localhost:8080/v1/admin/repository/establishment/options" -u root -v -d "{...}"

I get

    Enter host password for user 'root':
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
* Server auth using Basic with user 'root'
> POST /v1/admin/repository/establishment/options HTTP/1.1
> Host: localhost:8080
> Authorization: Basic cm9vdDpyb290
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 271
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 271 out of 271 bytes
< HTTP/1.1 401
< Set-Cookie: JSESSIONID=6E1CBD875597C83E6DEB794986050631; Path=/; HttpOnly
< 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
* Authentication problem. Ignoring this.
< WWW-Authenticate: Basic realm="Realm"
< Content-Length: 0
< Date: Sun, 27 Sep 2020 15:29:13 GMT
<
* Connection #0 to host localhost left intact
* Closing connection 0

The same user/pass combination works without issue on GET. What do?

2 Answers

The reason is that CSRF defense is switched on by default for httpBasic as well as formLogin. Because your POST does not contain a CSRF token, Spring Security is rejecting it.

You can get a better idea of this by changing your configuration to permit the /error endpoint:

http
    .authorizeRequests()
        .antMatchers("/", "/v1/occupancy/**", "/error").permitAll()
    // ...

Then, when you submit a POST, you'll see a 403 instead, related to the CSRF token denial.

If your REST API isn't front-facing, then you may be able to switch off CSRF. One way you can do this is by disabling the appropriate endpoints:

http
  .authorizeRequests()
    // ...
    .and()
  .csrf()
    .ignoringAntMatchers("/v1/admin/repository/establishment/options")
    // ...

It if is front-facing, then the appropriate thing to do is prepare your front-end to hand the CSRF token back to the request.

The configuration require more steps.

  1. configure String security

    @Configuration @EnableWebSecurity @EnableTransactionManagement public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

     @Autowired
     private UserDetailServiceImpl userService;
    
     @Autowired
     public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
         auth.userDetailsService(userService);
     }
    
    
    
    
     @Override
     protected void configure(HttpSecurity http) throws Exception {
    
         http.authorizeRequests()
                 .antMatchers("/webjars/**", "/styles/**",  "/vendors/**", "/node_modules/**",  "/app/**", 
                         "/assets/**", "/api/channel/**", "/jobs/**").permitAll()
                 .anyRequest().authenticated()
             .and()
                 .formLogin().loginPage("/login").permitAll()
             .and()
                 .logout().logoutSuccessUrl("/login?logout").permitAll()
             .and()
                 .csrf().disable();
     }
    

    }

  2. the implement UserDetailsService @Service public class UserDetailServiceImpl implements UserDetailsService{ @Autowired UtentiRepostitory utentiRepository;

     @Override
     public UserDetails loadUserByUsername(String nomeUtente) throws UsernameNotFoundException {
         try {
    
             String userId = nomeUtente;
    
             Utente utente = utentiRepository.findById(userId).get();
    
             UserDetailsImp user = new UserDetailsImp(utente);
             user.username = nomeUtente;
    
             user.password = utente.getPasswd();
    
             return user;
         } catch (Exception e) {
             throw new UsernameNotFoundException(nomeUtente);
         }
     }
    

    } this service that you referenced into Spring Security configuration contains all the roles to generate an object that implements UserDetails interface

  3. Implement UserDetails interface

    public class UserDetailsImp implements UserDetails{

     /**
      * 
      */
     private static final long serialVersionUID = 1L;
     String password;
     String username;
     public Utente utente;
    
     public UserDetailsImp(Utente utente) {
         this.utente = utente;
     }
    
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {
         List<GrantedAuthority> res = new ArrayList<>();
         res.add(new SimpleGrantedAuthority("ROLE_MENU"));
         return res;
     }
    
     public String getPassword() {
         return password;
     }
    
     public void setPassword(String password) {
         this.password = password;
     }
    
     public String getUsername() {
         return username;
     }
    
     public void setUsername(String username) {
         this.username = username;
     }
    
    
     @Override
     public boolean isAccountNonExpired() {
         return true;
     }
    
     @Override
     public boolean isAccountNonLocked() {
         return true;
     }
    
     @Override
     public boolean isCredentialsNonExpired() {
         return true;
     }
    
     @Override
     public boolean isEnabled() {
         return true;
     }
    
     public String getDenominazione() {
         return utente.getDenominazione();
     }
    
    
    
     public Utente getUtente() {
         return utente;
     }
    
     public void setUtente(Utente utente) {
         this.utente = utente;
     }
    

    }

In my case @Autowired UtentiRepostitory utentiRepository; is a repository that reads data from a user database but you can use any meccanism you like. Now Spring security is set.

Then you need to add a form in the login page

<form class="form-horizontal" id="loginForm" th:action="@{/login}" method="post">
    <input type="text" name="username">
    <input type="password" name="password">
</form>

Don't gereget to add thymeleaf springsecurity dependency. I hope this may help

Related