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.