Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom login form. Configure Spring security to get a JSON response

I have a simple application which is splitted into 2 parts :

  • A backend which exposes REST services with Spring-boot / Spring-security
  • A frontend which contains only static files.

The requests are received by a nginx server which listens on port 80.

  • If the request URL begins with /api/, the request is redirected to the backend.
  • Else, the request is handled by nginx which serves the static files.

I created a custom login form (in the frontend part) and I am trying to configure the Spring-boot server.

There are a lot of examples where I can see how to define a "login success" url and a "login error" url but I do not want Spring-security to redirect the user. I want Spring-security to answer with HTTP 200 if the login succeeded or HTTP 40x is the login failed.

In other words : I want the backend to only answer with JSON, never HTML.

Up to now, when I submit the login form, the request is redirected and I get the default Spring login form as an answer.

I tried to use .formLogin().loginProcessingUrl("/login"); instead of loginPage("") :

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
      .withUser("user").password("password").roles("ADMIN");
  }

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .anyRequest().authenticated()
        .and()
      .formLogin()
        .loginProcessingUrl("/login");
like image 349
Arnaud Denoyelle Avatar asked Sep 10 '15 10:09

Arnaud Denoyelle


People also ask

How do I create a custom JSON response in spring boot?

Steps. Please create a new package with the name 'response' in which we will create a class with the name “ResponseHandler”. This class will later be used to generate responses, where the response will be received in the form of an object with 3 parameters/values ​​in it.

What does formLogin () do in Spring Security?

Form-based login is one form of Username/password authentication that Spring Security provides support for. This is provided through an Html form. Whenever a user requests a protected resource, Spring Security checks for the authentication of the request.

Can Spring be configured with a JSON file?

The Spring Boot framework provides a simple approach to load external JSON data through the command line. In case of need, we can load JSON data through properly configured PropertySourceFactory.


2 Answers

Thanks to M. Deinum and thanks to this guide, I could find the solution.

First, I had a configuration problem with the login form itself. As the backend has a context-path set to /api, the custom form should have submitted the form params to /api/login but I was actually submitting the data to /api/login/ (Notice the extra / at the end).

As a result, I was unknowingly trying to access a protected resource! Hence, the request was handled by the default AuthenticationEntryPoint which default behavior is to redirect the user to the login page.

As a solution, I implemented a custom AuthenticationEntryPoint :

private AuthenticationEntryPoint authenticationEntryPoint() {
  return new AuthenticationEntryPoint() {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
      httpServletResponse.getWriter().append("Not authenticated");
      httpServletResponse.setStatus(401);
    }
  };
}

Then used it in the configuration :

http
  .exceptionHandling()
  .authenticationEntryPoint(authenticationEntryPoint())

and I did the same for the other handlers :

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("user").password("password").roles("ADMIN");
  }

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
          .anyRequest().authenticated()
        .and()
          .formLogin()
          .successHandler(successHandler())
          .failureHandler(failureHandler())
        .and()
          .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler())
            .authenticationEntryPoint(authenticationEntryPoint())
        .and()
          .csrf().csrfTokenRepository(csrfTokenRepository()).and().addFilterAfter(csrfHeaderFilter(), CsrfFilter.class)
    ;
  }

  private AuthenticationSuccessHandler successHandler() {
    return new AuthenticationSuccessHandler() {
      @Override
      public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.getWriter().append("OK");
        httpServletResponse.setStatus(200);
      }
    };
  }

  private AuthenticationFailureHandler failureHandler() {
    return new AuthenticationFailureHandler() {
      @Override
      public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.getWriter().append("Authentication failure");
        httpServletResponse.setStatus(401);
      }
    };
  }

  private AccessDeniedHandler accessDeniedHandler() {
    return new AccessDeniedHandler() {
      @Override
      public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.getWriter().append("Access denied");
        httpServletResponse.setStatus(403);
      }
    };
  }

  private AuthenticationEntryPoint authenticationEntryPoint() {
    return new AuthenticationEntryPoint() {
      @Override
      public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.getWriter().append("Not authenticated");
        httpServletResponse.setStatus(401);
      }
    };
  }

  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;
  }
}
like image 114
Arnaud Denoyelle Avatar answered Oct 09 '22 12:10

Arnaud Denoyelle


Here's configuration for Spring Boot 2.2.5.RELEASE:

package com.may.config.security;

import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user").password(passwordEncoder().encode("user")).roles("USER").and()
            .withUser("admin").password(passwordEncoder().encode("admin")).roles("USER", "ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .requestCache().disable() // do not preserve original request before redirecting to login page as we will return status code instead of redirect to login page (this is important to disable otherwise session will be created on every request (not containing sessionId/authToken) to non existing endpoint aka curl -i -X GET 'http://localhost:8080/unknown')
            .authorizeRequests()
                .antMatchers("/health", "/swagger-ui.html/**", "/swagger-resources/**", "/webjars/springfox-swagger-ui/**", "/v2/api-docs").permitAll()
                .anyRequest().hasRole("USER").and()
            .exceptionHandling()
                .accessDeniedHandler((req, resp, ex) -> resp.setStatus(SC_FORBIDDEN)) // if someone tries to access protected resource but doesn't have enough permissions
                .authenticationEntryPoint((req, resp, ex) -> resp.setStatus(SC_UNAUTHORIZED)).and() // if someone tries to access protected resource without being authenticated (LoginUrlAuthenticationEntryPoint used by default)
            .formLogin()
                .loginProcessingUrl("/login") // authentication url
                .successHandler((req, resp, auth) -> resp.setStatus(SC_OK)) // success authentication
                .failureHandler((req, resp, ex) -> resp.setStatus(SC_FORBIDDEN)).and() // bad credentials
            .sessionManagement()
                .invalidSessionStrategy((req, resp) -> resp.setStatus(SC_UNAUTHORIZED)).and() // if user provided expired session id
            .logout()
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()); // return status code on logout
    }

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

Important aspects here:

http.requestCache().disable()

important to disable otherwise new session will be created on every request to non existing endpoint (e.g. curl -i -X GET 'http://localhost:8080/unknown')

at least this is how it works with spring-session configured in the project

if not overridden - ExceptionTranslationFilter will use requestCache to preserve original URL to session (that creates session if non is exist) while handling AccessDeniedException.

http.sessionManagement().invalidSessionStrategy((req, resp) -> resp.setStatus(SC_UNAUTHORIZED))

return 401 status code in case user supplied expired sessionId in request

if not overridden - fallbacks to authenticationEntryPoint

can be helpful to provide meaningful message in response (aka "Your session has expired")

http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());

return 200 status code on logout

if not overridden - redirects web client to login page

like image 32
Eugene Maysyuk Avatar answered Oct 09 '22 12:10

Eugene Maysyuk