0

I'm developing a RESTful web service and had login working. I wanted to add security and access tokens so I added a UserDetailsService as seen below:

@Component
public class CustomLoginAuthenticationProvider implements UserDetailsService {

    @Autowired
    private BusinessUserService businessUserService;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        if(email == null || email.isEmpty() || !email.contains("@")) {
            System.out.println("ERROR - THIS IS THE USERNAME:" + email);
            throw new UsernameNotFoundException(email);
        }
    //... More below, but it doesn't matter. Exception thrown every time
}

However, the email string is empty. I can't understand why because I'm having a hard time understanding exactly when this method is called and what value is sent as the parameter for this method as this is a REST back end being sent JSON. Here's my WebSecurityConfigurerAdapter setup:

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Bean
    public CustomLoginAuthenticationProvider customLoginAuthenticationProvider() {
        return new CustomLoginAuthenticationProvider();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http
          .authorizeRequests()
          .antMatchers("/css/**", "/js/**", "/images/**", "/fonts/**", 
                       "/videos/**", "/", "/register", "/login", "/about", 
                       "/contact", "/test")
          .permitAll()
        .and()
          .authorizeRequests()
          .anyRequest()
          .authenticated()                            
        .and()
          .exceptionHandling()
          .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))
        .and()
          .formLogin()
          .loginPage("/login")
          .loginProcessingUrl("/login")
          .usernameParameter("email")
          .passwordParameter("password")
        .and()
          .logout()
          .logoutSuccessUrl("/")
          .permitAll()
        .and()
          .csrf()
          .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        .and()
          .addFilterAfter(new CsrfTokenFilter(), CsrfFilter.class);
    }
}

I specify that an email should be sent to the method when I use .usernameParameter("email") so I'm not really sure why it's not populating the email parameter. I'm using AngularJS in the front end and sending the credentials to the back end using JSON.

Jake Miller
  • 2,432
  • 2
  • 24
  • 39

2 Answers2

2

If you are sending the credentials in json, the error here is due to the fact that the email is not a http parameter, is included into the RequestBody.

The default UsernamePasswordAuthenticationFilter takes the credentials from http param:

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {


    public Authentication attemptAuthentication(HttpServletRequest request,
                HttpServletResponse response) throws AuthenticationException {
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }

            String username = obtainUsername(request);
            String password = obtainPassword(request);
            ...
    }

    /**
         * Enables subclasses to override the composition of the password, such as by
         * including additional values and a separator.
         * <p>
         * This might be used for example if a postcode/zipcode was required in addition to
         * the password. A delimiter such as a pipe (|) should be used to separate the
         * password and extended value(s). The <code>AuthenticationDao</code> will need to
         * generate the expected password in a corresponding manner.
         * </p>
         *
         * @param request so that request attributes can be retrieved
         *
         * @return the password that will be presented in the <code>Authentication</code>
         * request token to the <code>AuthenticationManager</code>
         */
        protected String obtainPassword(HttpServletRequest request) {
            return request.getParameter(passwordParameter);
        }

        /**
         * Enables subclasses to override the composition of the username, such as by
         * including additional values and a separator.
         *
         * @param request so that request attributes can be retrieved
         *
         * @return the username that will be presented in the <code>Authentication</code>
         * request token to the <code>AuthenticationManager</code>
         */
        protected String obtainUsername(HttpServletRequest request) {
            return request.getParameter(usernameParameter);
        }

You must write your own filter where you must read the incoming credentials from the RequestBody and set it in your configuration in the position of the UsernamePasswordAuthenticationFilter.

You could have a look at https://stackoverflow.com/a/35724932/4190848 and https://stackoverflow.com/a/35699200/4190848

Community
  • 1
  • 1
jlumietu
  • 6,234
  • 3
  • 22
  • 31
2

Another solution, that is much easier, is to do this in your client application - works fine with Spring Security:

public login(username: string, password: string) {
    const headers = new Headers();
    headers.append('Content-Type', 'application/x-www-form-urlencoded');

    return this.http.post(
      `http://www.myserver.com:8080/myApp/login`,
      encodeURI(`username=${username}&password=${password}`),
      { headers }
    );
  }
}

This hides the parameters from the url and encodes them as form data, and spring security's default implementation likes this a lot.

Katie
  • 305
  • 3
  • 11
  • Make sure you are passing them as parameter instead of JSON data. An angular way of passing JSON data as parameter $http({ 'url': 'signup', 'method': 'POST', 'params': user }) – Ram Nov 04 '17 at 19:35
  • Worked perfectly for me, thanks :) – Sofiane Apr 12 '22 at 11:21