0

I am trying to implement a custom login page with Spring Boot and AngularJS that uses a REST call to authenticate. When I try to login, the POST method is being rejected with a 405 Method not allowed response. Here's what I have as my setup. I have set the loginProcessingUrl and set permitAll on the login form. Other than that, I've basically followed the example from this article. Here's my setup:

SecruityConfiguration.java

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Autowired
Environment env;
@Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/webjars/**", "/modules/**", "/*.js")
            .permitAll()
            .anyRequest().authenticated()
            .and()
        .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
            .and()
        .formLogin()
            .permitAll()
            .loginPage("/login")
            .loginProcessingUrl("/api/authentication/login")
            .successHandler(authenticationSuccessHandler)
            .failureHandler(authenticationFailureHandler)
            .and()
        .logout()
            .logoutUrl("/api/authentication/logout")
            .logoutSuccessUrl("/login")
            .and()
        .csrf().csrfTokenRepository(csrfTokenRepository())
            .and()
        .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}
//Other code below...

Authentication Entry Point

@Component
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)
    throws IOException, ServletException {

        httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}

Failure Handler

@Component
public class RESTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

@Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)
    throws IOException, ServletException {

    super.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
}
}

Success Handler

@Component
public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
    throws IOException, ServletException {

    clearAuthenticationAttributes(request);
}
}

I cannot see what I'm missing with this setup, it seems it should work. If any other code is needed I'm happy to provide. Thanks for any help!

Edit: Adding Active Directory LDAP code for authenticating. Code exists in SecurityConfiguration.java

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
    ActiveDirectoryLdapAuthenticationProvider ldapAuthenticationProvider =
            new ActiveDirectoryLdapAuthenticationProvider(env.getProperty("ldap.domain"), env.getProperty("ldap.url"));
    ldapAuthenticationProvider.setConvertSubErrorCodesToExceptions(true);
    ldapAuthenticationProvider.setUseAuthenticationRequestCredentials(true);
    ldapAuthenticationProvider.setUserDetailsContextMapper(userDetailsContextMapper());

    auth.authenticationProvider(ldapAuthenticationProvider);
}

@Bean
public UserDetailsContextMapper userDetailsContextMapper() {
    return new LDAPDetailsContextMapper();
}

The login page sends a credentials object like so: {"username":"foo","password":"bar"}

Edit: Adding Angular code for login

Angular Service

function login(credentials) {
        var deferred = $q.defer();

        $http.post('/api/authentication/login', credentials)
            .success(loginComplete)
            .error(loginFailed);

        // Promise for successful response.  Return data to the controller
        function loginComplete(data) {
            user = data;
            deferred.resolve(data);
        }

        // Promise for failed response.  Logs error to console.
        function loginFailed(err) {
            deferred.reject(err, credentials);
        }

        // returns the promise
        return deferred.promise;

Angular Controller

function login() {
        var credentials = {username: vm.username, password: vm.password};

        if (validateRequiredFields()) {
            authenticationService.login(credentials)
                .then(loginSuccess)
                .catch(loginFailed);
        }

        function loginSuccess(user) {
            $rootScope.user = user;

            //change route here
            var redirectUrl = '';
            if ($location.search().redirect) {
                redirectUrl = $location.search().redirect;
            } else {
                redirectUrl = '/';
            }
            $location.url(redirectUrl);
        }
nateha1984
  • 99
  • 3
  • 14

2 Answers2

0

"Method not allowed" happens also when you don't have certain HTTP method implemented in controller.

As you didn't show us controller code, you may be missing method attribute of @RequestMapping annotation.

luboskrnac
  • 23,973
  • 10
  • 81
  • 92
  • My understanding is that Spring handles the login request and passes the username and password to whichever service you're authenticating against. Therefore, I wouldn't need a separate login controller, would I? I added the code showing the Authentication Provider setup if that's necessary. – nateha1984 May 25 '16 at 18:08
  • If you are creating web API with Spring, you most likely want to use Spring MVC controller. I was referring to Spring MVC controller not angular one. – luboskrnac May 25 '16 at 21:02
0

It runs out the 405 error was a red herring. The issue was the username and password was being sent as an application/json request which was not expected by Spring Boot. When the UsernamePasswordAuthenticationFilter tried to get the username, it couldn't find it. I assume that it then passed that exception down the rest of the filter chain and somehow the 405 error was the result.

Solution: Extend the UsernamePasswordAuthenticationFilter to parse the JSON object then pass those credentials to the Authentication object as discussed in this post.

Community
  • 1
  • 1
nateha1984
  • 99
  • 3
  • 14