37

I've an application in spring/spring-mvc that totally uses JSON communications. Now I need to authenticate my application with spring security 3 (that uses LdapAuthenticationProvider) via JSON.

The default spring seurity submit form requires a POST like this:

POST /myapp/j_spring_security_check HTTP/1.1
Accept-Encoding: gzip,deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
Host: 127.0.0.1:8080
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.1 (java 1.5)

j_username=myUsername&j_password=myPass

But I want to pass a JSON object like this:

{"j_username":"myUsername","j_password":"myPass"}

I read many post like this, this other or this one without luck, in all ajax cases is done a POST like above.

Any Ideas?

Community
  • 1
  • 1
fl4l
  • 1,580
  • 4
  • 21
  • 27

8 Answers8

25

According with Kevin suggestions,
and after reading this posts: 1, 2, documentation 3, and thanks to this blog post,
I wrote my own FORM_LOGIN_FILTER to directly manage JSON before authentication.
I paste my code for the community.

The goal is to grant both the classical browser form POST authentication with JSON based authentication. Also in JSON authentication I want to avoid the redirect to loginSuccesful.htm

In context:

<security:http use-expressions="true" auto-config="false" entry-point-ref="http403EntryPoint">      
    <security:intercept-url pattern="/logs/**" access="denyAll" />
    <!-- ... All other intercept URL -->

    <security:custom-filter ref="CustomUsernamePasswordAuthenticationFilter" position="FORM_LOGIN_FILTER "/>
    <security:logout
            invalidate-session="true"
            logout-success-url="/LogoutSuccessful.htm"
            delete-cookies="true"
    />
    <security:session-management>
        <security:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
    </security:session-management>
    <security:access-denied-handler error-page="/accessDenied.htm" />
</security:http>

<bean id="CustomUsernamePasswordAuthenticationFilter" class="path.to.CustomUsernamePasswordAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
    <property name="authenticationSuccessHandler" ref="customSuccessHandler"/>
    <property name="authenticationFailureHandler" ref="failureHandler"/>
    <property name="filterProcessesUrl" value="/j_spring_security_check"/>
    <property name="usernameParameter" value="j_username"/>
    <property name="passwordParameter" value="j_password"/>
</bean>

<bean id="customSuccessHandler" class="path.to.CustomAuthenticationSuccessHandler">
    <property name="defaultTargetUrl" value="/login.htm" />
    <property name="targetUrlParameter" value="/LoginSuccessful.htm" />
</bean>

<bean id="failureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
    <property name="defaultFailureUrl" value="/login.htm" />
</bean>

<bean id="http403EntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" />

CustomUsernamePasswordAuthenticationFilter class:

public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter{
    private String jsonUsername;
    private String jsonPassword;

    @Override
    protected String obtainPassword(HttpServletRequest request) {
        String password = null; 

        if ("application/json".equals(request.getHeader("Content-Type"))) {
            password = this.jsonPassword;
        }else{
            password = super.obtainPassword(request);
        }

        return password;
    }

    @Override
    protected String obtainUsername(HttpServletRequest request){
        String username = null;

        if ("application/json".equals(request.getHeader("Content-Type"))) {
            username = this.jsonUsername;
        }else{
            username = super.obtainUsername(request);
        }

        return username;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){
        if ("application/json".equals(request.getHeader("Content-Type"))) {
            try {
                /*
                 * HttpServletRequest can be read only once
                 */
                StringBuffer sb = new StringBuffer();
                String line = null;

                BufferedReader reader = request.getReader();
                while ((line = reader.readLine()) != null){
                    sb.append(line);
                }

                //json transformation
                ObjectMapper mapper = new ObjectMapper();
                LoginRequest loginRequest = mapper.readValue(sb.toString(), LoginRequest.class);

                this.jsonUsername = loginRequest.getUsername();
                this.jsonPassword = loginRequest.getPassword();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return super.attemptAuthentication(request, response);
    }
}

CustomAuthenticationSuccessHandler class:

public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    public void onAuthenticationSuccess(
            HttpServletRequest request,
            HttpServletResponse response,
            Authentication auth
    )throws IOException, ServletException {

        if ("application/json".equals(request.getHeader("Content-Type"))) {
            /*
             * USED if you want to AVOID redirect to LoginSuccessful.htm in JSON authentication
             */         
            response.getWriter().print("{\"responseCode\":\"SUCCESS\"}");
            response.getWriter().flush();
        } else {
            super.onAuthenticationSuccess(request, response, auth);
        }
    }
}
Community
  • 1
  • 1
fl4l
  • 1,580
  • 4
  • 21
  • 27
  • 21
    This is not thred safe. The filter is a bean. You should not store the username and password as members i would suggest to save as request attribute or not override obtainUsername and obtainPassword. Se my solution – oe.elvik Dec 03 '13 at 14:05
  • 1
    Yup, I tried this solution out and had this exact issue. Using request attributes instead of member variables fixed the problem. – Keeth Dec 09 '13 at 20:15
  • 1
    You are in the right direction, but this is a terrible implementation. As @oe.elvik mentioned, you can't have instance variables in filters! Just override attemptAuthentication and read username/password there. No need to use obtainPassword/obtainUsername. Also don't store password on request.. or anywhere else! – pakman Sep 28 '18 at 18:03
20
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        LoginRequest loginRequest = this.getLoginRequest(request);

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());

        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    private LoginRequest getLoginRequest(HttpServletRequest request) {
        BufferedReader reader = null;
        LoginRequest loginRequest = null;
        try {
            reader = request.getReader();
            Gson gson = new Gson();
            loginRequest = gson.fromJson(reader, LoginRequest.class);
        } catch (IOException ex) {
            Logger.getLogger(AuthenticationFilter.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                reader.close();
            } catch (IOException ex) {
                Logger.getLogger(AuthenticationFilter.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        if (loginRequest == null) {
            loginRequest = new LoginRequest();
        }

        return loginRequest;
    }
}
oe.elvik
  • 507
  • 2
  • 10
  • OK it's a valid solution. The goal in my class was to respect the original attemptAuthentication flow retrieving and setting username and password and then calling super.attemptAuthentication(...) at the end of the method. Thanks – fl4l Dec 04 '13 at 13:29
  • 5
    Yes, but by saving username and password as members of the bean the solution is not treadsafe. Two users trying to authenticate att the same time might mix username and passwords. One solution would also be saving the username and password in the request object using setAttribute – oe.elvik Dec 06 '13 at 11:49
14

You can write your own security filter that will parse your JSON.

http://docs.spring.io/spring-security/site/docs/3.0.x/reference/core-web-filters.html

You can use the BasicAuthenticationFilter as a reference:

http://docs.spring.io/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.html

Kevin Bayes
  • 846
  • 11
  • 17
10

If you want just different request body parser for login request just extend UsernamePasswordAuthenticationFilter and override attemptAuthentication method. By default UsernamePasswordAuthenticationFilter will parse url encoded data and create UsernamePasswordAuthenticationToken from it. Now you just need to make parser that will parse whatever you send to application.

Here is example that will parse {"username": "someusername", "password": "somepassword"}

public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            BufferedReader reader = request.getReader();
            StringBuffer sb = new StringBuffer();
            String line = null;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            String parsedReq = sb.toString();
            if (parsedReq != null) {
                ObjectMapper mapper = new ObjectMapper();
                AuthReq authReq = mapper.readValue(parsedReq, AuthReq.class);
                return new UsernamePasswordAuthenticationToken(authReq.getUsername(), authReq.getPassword());
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
            throw new InternalAuthenticationServiceException("Failed to parse authentication request body");
        }
        return null;
    }

    @Data
    public static class AuthReq {
        String username;
        String password;
    }

}

In snippet request body is extracted to string and mapped to object AuthReq (@Data annotation is from lombok lib, it will generate seters and getters). Than you can make UsernamePasswordAuthenticationToken that will be passed to default AuthenticationProvider.

Now you can extend WebSecurityConfigurerAdapter and override cnofigure method to replace old filter.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/", "/login", "/logout").permitAll()
            .anyRequest().authenticated()
        .and().addFilterAt(new CustomUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
        .formLogin().loginProcessingUrl("/login")
        .and()
        .csrf().disable();
}

With addFilterAt method you replace default UsernamePasswordAuthenticationFilter. Dont forget to use @EnableWebSecurity annotation.

Mihael Mamula
  • 330
  • 3
  • 11
  • 1
    maybe spring has changed. returning UsernamePasswordAuthenticationToken from attemptAuthentication seems incorrect. – Maxi Wu Jun 19 '19 at 06:51
  • As shown above should be `return this.getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(authReq.getUsername(), authReq.getPassword());` – MarCrazyness Jun 03 '22 at 17:44
5

Another way, according with this post, is to manage manually the spring security authentication directly in the Controller.
In this manner is very simple to manage JSON input and avoid login redirect:

@Autowired
AuthenticationManager authenticationManager;

@ResponseBody
@RequestMapping(value="/login.json", method = RequestMethod.POST)
public JsonResponse mosLogin(@RequestBody LoginRequest loginRequest, HttpServletRequest request) {
    JsonResponse response = null;

    try {
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
        token.setDetails(new WebAuthenticationDetails(request));

        Authentication auth = authenticationManager.authenticate(token);
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(auth);

        if(auth.isAuthenticated()){
            HttpSession session = request.getSession(true);
            session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);

            LoginResponse loginResponse = new LoginResponse();
            loginResponse.setResponseCode(ResponseCodeType.SUCCESS);
            response = loginResponse;   
        }else{
            SecurityContextHolder.getContext().setAuthentication(null);

            ErrorResponse errorResponse = new ErrorResponse();
            errorResponse.setResponseCode(ResponseCodeType.ERROR);
            response = errorResponse;
        }   
    } catch (Exception e) {     
        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setResponseCode(ResponseCodeType.ERROR);
        response = errorResponse;           
    }
    return response;
}
Community
  • 1
  • 1
fl4l
  • 1,580
  • 4
  • 21
  • 27
  • 2
    I would prefer the filter, the above is basically just a request in a controller and I feel it should not be mixed and the filter gives a nice separation between security and the controllers. Personal opinion. – Kevin Bayes Oct 25 '13 at 08:35
  • I have the same opinion, i prefer filters because are in the "security layer" that is managed before coming into controllers. From the point of view of security, you think that this solution is less secure than the filter solution? is there a potential security exposition? – fl4l Oct 27 '13 at 20:19
  • don't filters have the problem of not being a registered route, and spring can't properly handle other kinds of errors or features with them, like OPTIONS and HEAD requests, or showing all of the available routes. – xenoterracide Aug 06 '16 at 05:36
2

Look at this example: https://github.com/fuhaiwei/springboot_security_restful_api

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private CustomLoginHandler customLoginHandler;

    @Autowired
    private CustomLogoutHandler customLogoutHandler;

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .antMatchers("/api/basic/**").hasRole("BASIC")
                .antMatchers("/api/session").permitAll()
                .antMatchers(HttpMethod.GET).permitAll()
                .antMatchers("/api/**").hasRole("BASIC");

        http.formLogin();

        http.logout()
                .logoutUrl("/api/session/logout")
                .addLogoutHandler(customLogoutHandler)
                .logoutSuccessHandler(customLogoutHandler);

        http.exceptionHandling()
                .accessDeniedHandler(customAccessDeniedHandler)
                .authenticationEntryPoint(customAccessDeniedHandler);

        http.csrf()
                .ignoringAntMatchers("/api/session/**");

        http.addFilterBefore(new AcceptHeaderLocaleFilter(), UsernamePasswordAuthenticationFilter.class);

        http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
    }

    private CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
        filter.setAuthenticationSuccessHandler(customLoginHandler);
        filter.setAuthenticationFailureHandler(customLoginHandler);
        filter.setAuthenticationManager(authenticationManager());
        filter.setFilterProcessesUrl("/api/session/login");
        return filter;
    }

    private static void responseText(HttpServletResponse response, String content) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
        response.setContentLength(bytes.length);
        response.getOutputStream().write(bytes);
        response.flushBuffer();
    }

    @Component
    public static class CustomAccessDeniedHandler extends BaseController implements AuthenticationEntryPoint, AccessDeniedHandler {
        // NoLogged Access Denied
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
            responseText(response, errorMessage(authException.getMessage()));
        }

        // Logged Access Denied
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
            responseText(response, errorMessage(accessDeniedException.getMessage()));
        }
    }

    @Component
    public static class CustomLoginHandler extends BaseController implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
        // Login Success
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
            LOGGER.info("User login successfully, name={}", authentication.getName());
            responseText(response, objectResult(SessionController.getJSON(authentication)));
        }

        // Login Failure
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
            responseText(response, errorMessage(exception.getMessage()));
        }
    }

    @Component
    public static class CustomLogoutHandler extends BaseController implements LogoutHandler, LogoutSuccessHandler {
        // Before Logout
        @Override
        public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

        }

        // After Logout
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
            responseText(response, objectResult(SessionController.getJSON(null)));
        }
    }

    private static class AcceptHeaderLocaleFilter implements Filter {
        private AcceptHeaderLocaleResolver localeResolver;

        private AcceptHeaderLocaleFilter() {
            localeResolver = new AcceptHeaderLocaleResolver();
            localeResolver.setDefaultLocale(Locale.US);
        }

        @Override
        public void init(FilterConfig filterConfig) {
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            Locale locale = localeResolver.resolveLocale((HttpServletRequest) request);
            LocaleContextHolder.setLocale(locale);

            chain.doFilter(request, response);
        }

        @Override
        public void destroy() {
        }
    }    
}



public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        UsernamePasswordAuthenticationToken authRequest;
        try (InputStream is = request.getInputStream()) {
            DocumentContext context = JsonPath.parse(is);
            String username = context.read("$.username", String.class);
            String password = context.read("$.password", String.class);
            authRequest = new UsernamePasswordAuthenticationToken(username, password);
        } catch (IOException e) {
            e.printStackTrace();
            authRequest = new UsernamePasswordAuthenticationToken("", "");
        }
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

}
Intopass
  • 71
  • 3
1

I applied the answers from fl4l and oe.elvik for login with JSON credentials in a Spring Boot application. I am working with annotation-based bean configuration.

In the referenced answers, a custom filter is created in which the authentication manager is injected. To do this, the authentication manager must be present as a Spring Bean. Here is a link on how to do that: https://stackoverflow.com/a/21639553/3950535.

Martijn Dirkse
  • 543
  • 7
  • 16
0

Here is the java configuration for the above solutions:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .addFilterBefore(authenticationFilter(),UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login")
            .permitAll();
}

@Bean
public AuthenticationFilter authenticationFilter() throws Exception{
    AuthenticationFilter authenticationFilter = new AuthenticationFilter();
    authenticationFilter.setUsernameParameter("username");
    authenticationFilter.setPasswordParameter("password");
    authenticationFilter.setAuthenticationManager(authenticationManager());
    authenticationFilter.setFilterProcessesUrl("/login");
    authenticationFilter.setAuthenticationSuccessHandler(successHandler());
    return authenticationFilter;
}

@Bean
public SuccessHandler successHandler(){
    return new SuccessHandler();
}
smali
  • 4,687
  • 7
  • 38
  • 60