Securing Stateless Web Service with Spring Security 3 and Crowd

With the help of Spring Security is the process of securing web application made simple. But to secure a stateless web service there are some parts that need to work differently or even some customizations. In this article I will discuss how to do this with the combination of Crowd as the user directories.


Since I am not going to discuss how to install Spring Security 3(SS3) in this article, I suggest you to check Spring Security documentation page if you have not yet installed your Spring Security.

Also to allow SS3 to work with Crowd, you need to modify the crowd-integation-client.jar. All you need to do is fix the import package names of Spring class, because SS3 have made some refactoring to their code since SS2. To get to the source code you can check this instruction. SS3 will also require you to provide the authentication manager as well. So add this to your context.xml.
    <security:authentication-manager>
<security:authentication-provider ref="crowdAuthenticationProvider"/>
</security:authentication-manager>

<bean id="crowdUserDetailsService"
class="com.atlassian.crowd.integration.springsecurity.user.CrowdUserDetailsServiceImpl">
<property name="authenticationManager" ref="crowdAuthenticationManager" />
<property name="groupMembershipManager" ref="crowdGroupMembershipManager" />
<property name="userManager" ref="crowdUserManager" />
<property name="authorityPrefix" value="ROLE_"/>
</bean>

<bean id="crowdAuthenticationProvider"
class="com.atlassian.crowd.integration.springsecurity.RemoteCrowdAuthenticationProvider">
<constructor-arg ref="crowdAuthenticationManager" />
<constructor-arg ref="httpAuthenticator" />
<constructor-arg ref="crowdUserDetailsService" />
</bean>


Now back to our main topic. There are 2 options you can go for if you want to secure your web service. First one is to use HTTP Basic authentication. This is an easy and simple solution, but not a good option if you have a publicly available web service. The second solution is to use an sso token that is managed by Crowd. This option is much more secured, but it requires some adjustments on the Spring Security itself.

HTTP Basic authentication


To enable this simply add http basic to your Spring Security setting
<security:http create-session="stateless">

<security:intercept-url pattern="/**" access="ROLE_USER" />
<security:http-basic />
</security:http>


This first option is a very simple solution that will work directly without having too much adjustments on your application. Further, all you need to do is send the HTTP Basic authentication along with every request. Of course the information in the HTTP Basic authentication is not in anyway secured, so this option is only good if the communications exist inside a secured contained network area.

Crowd SSO Token


To make this work you nedd to have a modified Spring Security setting such as:
<security:http create-session="stateless" entry-point-ref="unauthorizedEntryPoint">

<security:intercept-url pattern="/**" access="ROLE_USER" />
<security:custom-filter after="SECURITY_CONTEXT_FILTER" ref="customSecurityFilter"/>
<security:logout logout-url="/logout" success-handler-ref="customLogoutSuccessHandler"/>
</http>

<bean id="unauthorizedEntryPoint" class="nl.42.security.UnauthorizedEntryPoint"/>

<bean id="customSecurityFilter" class="nl.42.security.CustomSecurityFilter"/>

<bean id="customLogoutSuccessHandler" class="nl.42.security.CustomLogoutSuccessHandler"/>


As you see in the setting there are 3 custom parts added:

1. UnauthorizedEntryPoint that will serve as the point that will be executed when there is an unauthorized request found.
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
/**
* Always returns a 401 error code to the client.
*/
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException,
ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}


Normally for a web application the next step is to redirect user to the login page, but in the case of webservice we will just return an HTTP Response 401.

2. CustomSecurityFilter that check for the authentication/authorization of the incoming requests. This must be executed after the SECURITY_CONTEXT_FILTER (SecurityContextPersistenceFilter.class) because SecurityContextPersistenceFilter will use the existing security context if any(the standard SS3 save this in the HTTP Session) or create an empty context. But since we are making our service stateless there will not be a saved security context.
public class CustomSecurityFilter extends GenericFilterBean {

@Autowired
private CrowdUserDetailsService userDetailService;

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

SecurityContextHolder.setContext(loadSecurityContext(request));
chain.doFilter(request, response);
}

/**
* Get the proper security context. If a crowd token is found, create a CrowdTokenSecurityContext and use this as the current security context.
* Otherwise use the current security context.
*
* @param request HttpServletRequest
* @return A SecurityContext
*/
private SecurityContext loadSecurityContext(HttpServletRequest request) {
SecurityContext securityContext = SecurityContextHolder.getContext();
String token = CrowdTokenUtil.collectCrowdToken(request);
if (StringUtils.isNotEmpty(token)) {
try {
CrowdUserDetails crowdUserDetails = userDetailService.loadUserByToken(token);
Authentication crowdAuthentication = new CrowdSSOAuthenticationToken(crowdUserDetails, token, crowdUserDetails.getAuthorities());
securityContext = new CrowdTokenSecurityContext(crowdAuthentication);
} catch (CrowdSSOTokenInvalidException ex) {
// Ignore authentication failure. Use existing security context or let the rest of the filters manage the security context.
logger.info("The CrowdTokenAuthenticationFilter found an invalid token.");
} catch (DataAccessException ex) {
logger.info("The CrowdTokenAuthenticationFilter failed to verify the token provided.");
}
}
return securityContext;
}

}


Here you need to extract the token from the request. This can be done by using HTTP header authorization or other solution that you prefer. Then you collect the userDetails from Crowd and use this information to build the security context.
The CrowdSSOAuthenticationToken is an existing class inside the crowd-integration-client.jar and the CrowdTokenSecurityContext is a custom class that implements Spring SecurityContext.

As you can see also that I do not take any further action when Crowd complains about the token, because I want to treat the request with an invalid token as an unauthorized request. I want SS3 to take care of this problem and not having to deal it myself.

3. CustomLogoutSuccessHandler that will managed all cleaning up after the user logout.
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String crowdToken = CrowdTokenUtil.collectCrowdToken(request);
if (StringUtils.isNotEmpty(crowdToken)) {
try {
authenticationManager.invalidate(crowdToken);
} catch (Exception ex) {
// report to user that logout action failed? or ignore it?
}

}
}
}


The last step is to tell Crowd that the token needs to be invalidate. This is to make sure that the token cannot be used any longer.

The second option is a very secured solution, but of course you have to make sure that when you are requesting the token that you send this request using HTTPS protocol.

In the explanation above I did not mention how you can collect the Crowd SSO token, because this can vary depending on the architecture of your application. You can either choose for:

1. Using another application to get this token from the Crowd server.

2. Create a controller in your webservice to collect the token from the Crowd server.