18

I'm currently creating a simple app for a school project, Spring Boot backend and AngularJS frontend, but have a problem with security that I can't seem to solve.

Logging in works perfectly, but when I enter a wrong password the default login popup shows up, which is kind of annoying. I've tried the annotation 'BasicWebSecurity' and putting httpBassic on disabled, but with no result (meaning, that the login procedure doesn't work at all anymore).

My security class:

package be.italent.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

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

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    @Override
    public void configure(WebSecurity web){
        web.ignoring()
        .antMatchers("/scripts/**/*.{js,html}")
        .antMatchers("/views/about.html")
        .antMatchers("/views/detail.html")
        .antMatchers("/views/home.html")
        .antMatchers("/views/login.html")
        .antMatchers("/bower_components/**")
        .antMatchers("/resources/*.json");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()
                    .and()
                .authorizeRequests()
                .antMatchers("/user", "/index.html", "/", "/projects/listHome", "/projects/{id}", "/categories", "/login").permitAll().anyRequest()
                .authenticated()
                    .and()
                .csrf().csrfTokenRepository(csrfTokenRepository())
                    .and()
                .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class).formLogin();
    }

    private Filter csrfHeaderFilter() {
        return new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest request,
                                            HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
                CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class
                        .getName());
                if (csrf != null) {
                    Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
                    String token = csrf.getToken();
                    if (cookie == null || token != null
                            && !token.equals(cookie.getValue())) {
                        cookie = new Cookie("XSRF-TOKEN", token);
                        cookie.setPath("/");
                        response.addCookie(cookie);
                    }
                }
                filterChain.doFilter(request, response);
            }
        };
    }

    private CsrfTokenRepository csrfTokenRepository() {
        HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
        repository.setHeaderName("X-XSRF-TOKEN");
        return repository;
    }
}

Does anybody have an idea on how to prevent this popup from showing without breaking the rest?

solution

Added this to my Angular config:

myAngularApp.config(['$httpProvider',
  function ($httpProvider) {
    $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
  }
]);
abarisone
  • 3,707
  • 11
  • 35
  • 54
Dennie
  • 743
  • 2
  • 13
  • 30
  • your security config looks a bit confusing. First you activate httpBasic. then you want to use form login and then you want to disable httpBasic. For me its not clear WHAT you want to protect and in which way. THe csrfHeaderFilter looks like you have an angularJS frontend is that the case? – Yannic Bürgmann Jun 11 '16 at 15:15
  • Crap, I did a wrong copy/paste. The final httpBasic().disable() isn't supposed to be there. My bad! It's indeed an angularJS frontend. – Dennie Jun 11 '16 at 16:37
  • Ok. An answer will be provided in a few minutes. Hopefully this is what you want to achieve. If not just let me know in the comments. – Yannic Bürgmann Jun 11 '16 at 17:11
  • Is your problem solved now? – Yannic Bürgmann Jun 12 '16 at 06:53
  • Yes, thanks a lot, man! – Dennie Jun 12 '16 at 10:59

3 Answers3

29

Let's start with your problem

It is not a "Spring Boot security popup" its a browser popup that shows up, if the response of your Spring Boot app contains the following header:

WWW-Authenticate: Basic

In your security configuration a .formLogin() shows up. This should not be required. Though you want to authenticate through a form in your AngularJS application, your frontend is an independent javascript client, which should use httpBasic instead of form login.

How your security config could look like

I removed the .formLogin() :

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .httpBasic()
                .and()
            .authorizeRequests()
            .antMatchers("/user", "/index.html", "/", "/projects/listHome", "/projects/{id}", "/categories", "/login").permitAll().anyRequest()
            .authenticated()
                .and()
            .csrf().csrfTokenRepository(csrfTokenRepository())
                .and()
            .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}

How to deal with the browser popup

As previously mentioned the popup shows up if the response of your Spring Boot app contains the header WWW-Authenticate: Basic. This should not be disabled for all requests in your Spring Boot app, since it allows you to explore the api in your browser very easily.

Spring Security has a default configuration that allows you to tell the Spring Boot app within each request not to add this header in the response. This is done by setting the following header to your request:

X-Requested-With: XMLHttpRequest

How to add this header to every request made by your AngularJS app

You can just add a default header in the app config like that:

yourAngularApp.config(['$httpProvider',
  function ($httpProvider) {
    $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
  }
]);

The backend will now respond with a 401-response that you have to handle by your angular app (by an interceptor for example).

If you need an example how to do this you could have a look at my shopping list app. Its done with spring boot and angular js.

Yannic Bürgmann
  • 6,301
  • 5
  • 43
  • 77
  • when you are saying "add a default header in the app config", which does that mean? – SK. Apr 27 '18 at 14:04
  • The codesnippet i showed adds a Header to every request you do with $http automatically. This header prevents the Basic authentication popup – Yannic Bürgmann Apr 27 '18 at 18:09
  • Thanks for your reply. Just wondering if you can help me on this: https://stackoverflow.com/questions/50064389/angular-4-spring-boot-spring-security-getting-browser-authentication-popup – SK. Apr 27 '18 at 19:17
15

As Yannic Klem already said this is happening because of the header

WWW-Authenticate: Basic

But there is a way in spring to turn it off, and it's really simple. In your configuration just add:

.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint)

and since authenticationEntryPoint is not defined yet, autowire it at the beginning:

@Autowired private MyBasicAuthenticationEntryPoint authenticationEntryPoint;

And now create MyBasicAuthenticationEntryPoint.class and paste the following code:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
public class MyBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {

/**
 * Used to make customizable error messages and codes when login fails
 */
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx) 
  throws IOException, ServletException {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    PrintWriter writer = response.getWriter();
    writer.println("HTTP Status 401 - " + authEx.getMessage());
}

@Override
public void afterPropertiesSet() throws Exception {
    setRealmName("YOUR REALM");
    super.afterPropertiesSet();
}
}

Now your app wont send WWW-Authenticate: Basic header, because of that pop-up windows will not show, and there is no need to mess with headers in Angular.

Masinac
  • 151
  • 1
  • 3
6

As already explained above, the problem is in the header of response that is set with the values "WWW-Authenticate: Basic".

Another solution that can solve this is to implement the AuthenticationEntryPoint interface (directly) without placing these values in the header:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //(....)

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
        .csrf()
            .disable()
        .authorizeRequests()
            .antMatchers("/*.css","/*.js","/*.jsp").permitAll()
            .antMatchers("/app/**").permitAll()
            .antMatchers("/login").permitAll()

            .anyRequest().authenticated()

            .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/j_spring_security_check")
                .defaultSuccessUrl("/", true)
                .failureUrl("/login?error=true")
                .usernameParameter("username")
                .passwordParameter("password")
                .permitAll()
            .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .deleteCookies("JSESSIONID")
                .clearAuthentication(true)
                .invalidateHttpSession(true)
            .and()
                .exceptionHandling()
                .accessDeniedPage("/view/error/forbidden.jsp")
            .and()
                .httpBasic()
                .authenticationEntryPoint(new AuthenticationEntryPoint(){ //<< implementing this interface
                    @Override
                    public void commence(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException authException) throws IOException, ServletException {
                            //>>> response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\""); <<< (((REMOVED)))
                            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
                    }
                });
    }

    //(....)
}
Davijr
  • 1,093
  • 11
  • 10