29

Is there a way to disable the redirect for Spring Security and the login page. My requirements specify the login should be part of the navigation menu.

Example:

enter image description here

Therefore there is no dedicated login page. The login information needs to be submitted via Ajax. If an error occurs it should return JSON specifying the error and use the proper HTTP Status code. If authentication checks out it should return a 200 and then javascript can handle it from there.

I hope that makes sense unless there is any easier way to accomplish this with Spring Security. I don't have much experience with Spring Security. I assume this has to be a common practice, but I didn't find much.

Current spring security configuration

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/public/**").permitAll()
                .antMatchers("/about").permitAll()
                .anyRequest().fullyAuthenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .failureUrl("/login?error")
                .usernameParameter("email")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .deleteCookies("remember-me")
                .logoutSuccessUrl("/")
                .permitAll()
                .and()
                .rememberMe();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }

Update:

I tried using HttpBasic() but then it asks for login creds not matter what and its the ugly browser popup which is not acceptable to the end user. It looks like I may have to extend AuthenticationEntryPoint.

At the end of the day I need Spring security to send back JSON saying the authentication succeeded or failed.

greyfox
  • 6,426
  • 23
  • 68
  • 114
  • I think this will help http://stackoverflow.com/questions/11946903/how-can-i-disable-spring-form-based-login-for-restful-endpoints and this http://stackoverflow.com/questions/11985709/spring-security-without-form-login – Utkarsh Jul 30 '15 at 03:37
  • Maybe I am missing something but I still need to create a session so I don't think stateless would work for me, its not a Single Page Application – greyfox Jul 30 '15 at 03:40
  • The login form is part of the navigation menu, there is no login page. I don't want Spring security to redirect to a login page automatically when it encounters a 403 or 401. – greyfox Jul 30 '15 at 04:23
  • Then removing the `formLogin` with `httpBasic` should do what you required. You can add custom filters if you want more customisation apart from `httpBasic`. Like this `.and().httpBasic() .and()` – Utkarsh Jul 30 '15 at 04:26

4 Answers4

22

The redirect behavior comes from SavedRequestAwareAuthenticationSuccessHandler which is the default success handler. Thus an easy solution to remove the redirect is to write your own success handler. E.g.

http.formLogin().successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
       //do nothing
    }
});
ben
  • 5,671
  • 4
  • 27
  • 55
12

You need to disable redirection in a couple of different places. Here's a sample based on https://github.com/Apress/beg-spring-boot-2/blob/master/chapter-13/springboot-rest-api-security-demo/src/main/java/com/apress/demo/config/WebSecurityConfig.java

In my case, I don't return json body but only HTTP status to indicate success/failure. But you can further customize the handlers to build the body. I also kept CSRF protection on.

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    public void initialize(AuthenticationManagerBuilder auth, DataSource dataSource) throws Exception {
        // here you can customize queries when you already have credentials stored somewhere
        var usersQuery = "select username, password, 'true' from users where username = ?";
        var rolesQuery = "select username, role from users where username = ?";
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(usersQuery)
                .authoritiesByUsernameQuery(rolesQuery)
        ;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // all URLs are protected, except 'POST /login' so anonymous user can authenticate
            .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .anyRequest().authenticated()

            // 401-UNAUTHORIZED when anonymous user tries to access protected URLs
            .and()
                .exceptionHandling()
                .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))

            // standard login form that sends 204-NO_CONTENT when login is OK and 401-UNAUTHORIZED when login fails
            .and()
                .formLogin()
                .successHandler((req, res, auth) -> res.setStatus(HttpStatus.NO_CONTENT.value()))
                .failureHandler(new SimpleUrlAuthenticationFailureHandler())

            // standard logout that sends 204-NO_CONTENT when logout is OK
            .and()
                .logout()
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT))

            // add CSRF protection to all URLs
            .and()
                .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        ;
    }
}

Here's a deep explanation of the whole process, including CSRF and why you need a session: https://spring.io/guides/tutorials/spring-security-and-angular-js/

Scenarios that I tested:

happy path

GET /users/current (or any of your protected URLs)
 request --> no cookie
 <- response 401 + cookie XSRF-TOKEN

POST /login
 -> header X-XSRF-TOKEN + cookie XSRF-TOKEN + body form with valid username/password
 <- 204 + cookie JSESSIONID

GET /users/current
 -> cookie JSESSIONID
 <- 200 + body with user details

POST /logout
 -> header X-XSRF-TOKEN + cookie XSRF-TOKEN + cookie JSESSIONID
 <- 204

=== exceptional #1: bad credentials

POST /login
 -> header X-XSRF-TOKEN + cookie XSRF-TOKEN + body form with bad username/password
 <- 401

=== exceptional #2: no CSRF at /login (like a malicious request)

POST /login
 -> cookie XSRF-TOKEN + body form with valid username/password
 <- 401 (I would expect 403, but this should be fine)

=== exceptional #3: no CSRF at /logout (like a malicious request)

(user is authenticated)

POST /logout
 -> cookie XSRF-TOKEN + cookie JSESSIONID + empty body
 <- 403

(user is still authenticated)
André
  • 12,497
  • 6
  • 42
  • 44
5

On my project I implemented it for the requirements:

1) For rest-request 401 status if user is not authorized

2) For simple page 302 redirect to login page if user is not authorized

public class AccessDeniedFilter extends GenericFilterBean {

@Override
public void doFilter(
        ServletRequest request,
        ServletResponse response, FilterChain filterChain) throws IOException, ServletException {

    try {
        filterChain.doFilter(request, response);
    } catch (Exception e) {

        if (e instanceof NestedServletException &&
                ((NestedServletException) e).getRootCause() instanceof AccessDeniedException) {

            HttpServletRequest rq = (HttpServletRequest) request;
            HttpServletResponse rs = (HttpServletResponse) response;

            if (isAjax(rq)) {
                rs.sendError(HttpStatus.FORBIDDEN.value());
            } else {
                rs.sendRedirect("/#sign-in");
            }
        }
    }
}

private Boolean isAjax(HttpServletRequest request) {
    return request.getContentType() != null &&
           request.getContentType().contains("application/json") &&
           request.getRequestURI() != null &&
           (request.getRequestURI().contains("api") || request.getRequestURI().contains("rest"));
    }
}

And enable the filter:

@Override
protected void configure(HttpSecurity http) throws Exception {
   ...
    http
            .addFilterBefore(new AccessDeniedFilter(),
                    FilterSecurityInterceptor.class);
   ...
}

You can change handle AccessDeniedException for you requirements in the condition:

if (isAjax(rq)) {
    rs.sendError(HttpStatus.FORBIDDEN.value());
} else {
    rs.sendRedirect("/#sign-in");
}
Anton Nikanorov
  • 341
  • 3
  • 4
3

When a browser gets a 401 with "WWW-Authetication: Basic ... ", it pops up a Dialog. Spring Security sends that header unless it sees "X-Requested-With" in the request.

You should send "X-Requested-With: XMLHttpRequest" header for all requests, this is an old fashioned way of saying - I am an AJAX request.

Sid Sarasvati
  • 819
  • 9
  • 10