1

I have an angularjs front end and spring security at the back end.

My login controller sends via POST request the customer credentials that are encrypted using Base64 algorithm. The code is the following :

gasStation.controller('LoginController', ['$rootScope', '$scope', '$http', '$window', 'customerInformation',
    function ($rootScope, $scope, $http, $window, customerInformation) {
        $rootScope.Login = function () {
            var encodedData = btoa($scope.username+':'+$scope.password);
            $http.defaults.headers.common['Authorization'] = 'Basic ' + encodedData;

            $http({
                method: 'POST',
                url: '/login',
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "X-Ajax-call": 'true'
                }
            })
                .success(function (response) {
                })
                .error(function (response) {

                });
        };
    }]);

At the back end I have the following configuration of the Spring security:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();

        // declare all public resources and URIs
        http.authorizeRequests()
                .antMatchers("/pages/index.html", "/pages/public/**", "/resources/css/**", "/resources/img/**", "/resources/js/**").permitAll();
        http.authorizeRequests().antMatchers("/login", "logout").permitAll();
        http.authorizeRequests().antMatchers(HttpMethod.POST, "/register").permitAll();
        http.authorizeRequests().antMatchers(HttpMethod.GET, "/customer_types").permitAll();

        // any other resources and URIs must pass authentication procedure.
        http.httpBasic().and().authorizeRequests().anyRequest().authenticated();
        http.formLogin()
                .successHandler(new AjaxAuthenticationSuccessHandler(customerRepository))
                .failureHandler(new AjaxAuthenticationFailureHandler())
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/pages/index.html");

        http.exceptionHandling().authenticationEntryPoint(new AjaxAuthorizationPoint());
    }

If authentication is successful then I send back a cookie:

public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private CustomerRepository customerRepository;

    public AjaxAuthenticationSuccessHandler(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        int numberOfEntries;
        ObjectMapper objectMapper = new ObjectMapper();
        CustomerInformationDto customerInformationDto = new CustomerInformationDto();

        Customer customer = customerRepository.getByLogin(authentication.getName());
        String customerType = customer.getCustomerType().getTypeName();
        if ("REGULAR".equals(customerType)) {
            numberOfEntries = customer.getVehicles().size();
        } else {
            numberOfEntries = customer.getGasstations().size();
        }

        // create here a cookie and send it back to a client.

        customerInformationDto.setStatus("ok");
        customerInformationDto.setCustomerType(customer.getCustomerType().getTypeName());
        customerInformationDto.setNumberOfEntries(numberOfEntries);

        response.getWriter().print(objectMapper.writeValueAsString(customerInformationDto));
        saveCookie("my god damn cookie","my god damn cookie",response);

        response.getWriter().flush();
    }
    private void saveCookie(String cookieName, String value, HttpServletResponse response) {
        Cookie cookie = new Cookie(cookieName, value);
        //maxAge is one month: 30*24*60*60
        cookie.setMaxAge(2592000);
        response.addCookie(cookie);
    }
}

If something is wrong then I simply send back an error message:

public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        ObjectMapper objectMapper = new ObjectMapper();
        CustomerInformationDto customerInformationDto = new CustomerInformationDto();
        customerInformationDto.setStatus("Invalid login or password.");
        response.setStatus(403);
        response.getWriter().print(objectMapper.writeValueAsString(customerInformationDto));
        response.getWriter().flush();
    }
}

However, if I send a valid login and password that are encrypted with base64 then my UserDetailsService cannot find a customer by his/her login thus causing me a 403 error.

The question is: how does the Spring decode the login and password from the Authorization header?

Put it in another way, when I use the following code (without base64):

gasStation.controller('LoginController', ['$rootScope', '$scope', '$http', '$window', 'customerInformation',
    function ($rootScope, $scope, $http, $window, customerInformation) {
        $rootScope.Login = function () {
            var postData = 'username=' + $scope.username + '&password=' + $scope.password;
            var url = "http://" + $window.location.host + '/pages/index.html#';


            $http({
                method: 'POST',
                url: '/login',
                data:postData,
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "X-Ajax-call": 'true'
                }
            })
                .success(function (response) {

                })
                .error(function (response) {
                    $scope.errorMessage = response.status;
                });
        };
    }]);

Spring successfully finds a user with his/her login, but when I use a btoa encryption at front end - it fails to do so.

mr.M
  • 851
  • 6
  • 23
  • 41
  • You shouldn't send a Base64 encrypted password while login. Inject the encoder bean in your authentication-manager and send password without Base64, then it will work. This is the reason why https is required at login pages. – We are Borg Oct 28 '15 at 13:15
  • Could you elaborate on what you mean by saying `inject the encoder bean in your authentication-manager`? Do you mean that I should autowire any any password encoder in my spring security context configuration? – mr.M Oct 28 '15 at 13:25
  • Not any, the one which has the Base64 encoding. I have examples only with XML config, not Java config. – We are Borg Oct 28 '15 at 13:28
  • So, if I do inject this Base64 encoder then the password will be encoded from a raw to a base64 encoding. – mr.M Oct 28 '15 at 13:33
  • Yes, exactly, and also while saving passwords in the DB, you will have to Base64 encode/encrypt them. Injecting them tells Spring I am using Base64 encoding. Got it? – We are Borg Oct 28 '15 at 13:35
  • Yep. Just to recap: 1. I must send a raw password, I cannot transfer a encoded password 2. Thus I have to use the HTTPS channel 3. At the back-end, I have to store the encrypted password. – mr.M Oct 28 '15 at 13:37

1 Answers1

1

First a nitpick: Base64 is an encoding algorithm, not encryption. But I don't think that base64 is the problem, that part looks fine.

The problem is that you use the same URL for Basic auth as for form login (/login). The request will hit the UsernamePasswordAuthenticationFilter first, which will fail the authentication since there is no form data. The Authorization header is never checked. By doing the Basic auth against another URL path will fix the problem.

Also note that AuthenticationFailureHandler is for form login only, it won't be invoked when correctly using Basic auth.

holmis83
  • 15,922
  • 5
  • 82
  • 83
  • If the problem is in url "/login" then where should I declare another url (suppose "/ajax_login") in my security context configuration? – mr.M Oct 28 '15 at 16:14
  • @mr.M You don't need to declare an URL for Basic auth, it is available for all URL:s that isn't reserved by Spring Security filters (login, logout). – holmis83 Oct 28 '15 at 16:21
  • Ok. You were right about the URL naming. Now, my UserDetailsService finds the user name from a database. However, I would like to send back some data with the 200 response status. Where I can define a filter and put it in the security filter chain to accomplish this task? – mr.M Oct 28 '15 at 16:28
  • @mr.M You can add a mapping in your controller? Or look at this: http://stackoverflow.com/questions/16734537/spring-security-3-http-basic-authentication-success-handler – holmis83 Oct 28 '15 at 16:35
  • Thank your for your suggestions. Everything works like a charm! – mr.M Oct 28 '15 at 16:56