Spring Security and JSON Authentication

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);
        }
    }
}

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;
    }
}

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


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.