Spring Security comes with Preauthentication framework that
allows authentication using an external authenticated system ( Single Sign-On solutions- IBM’s WebSEAL, CA Siteminder) while still using internal authentication provider for authorization.
Spring’s preauthentication framework reads SSO
security tokens (populated by external SSO provider) and populates user
identity in its context, authorities and UserDetails are still loaded
by authentication providers (JDBC, LDAP..) configured in security
configuration.
Usually, enterprise SSO solutions are expensive and their
licenses are not always extended to developer’s sandbox. As a result, workarounds
are devised for developers that can compromise security, like hard-coding/commenting
authentication piece( imagine if that makes it to production). Also, there might be times when production support requires
direct access to the application without going through SSO channel (SSO systems
can be buggy too :)).
We implemented a configurable SSO emulator using spring security's filter chain
that can mimic external authentication system on developer’s sandboxes. In SSO
enabled environment the request is authenticated by external authentication system while on
non-SSO environments it is authenticated by internal providers. In
both cases the authorization (userdetailsservice) was common and agnostic of
the authentication mechanism.
Spring Security makes every incoming request pass through a
filter chain, where each filter inspects and validates the request based on it
configuration before handing it over to next filter. Spring allows you to write you own custom
filters and place them in the filter chain. You have to be really careful with
the sequencing of your filters because there are inter dependencies between them. I had to spent a lot of time to get the filter
sequencing right :)
We configured two custom filters and place them
infront of preautheticated filter.
First filter (CustomFormAuthenticationFIlter)
inspects the request to check where it is coming from:
- If it was coming from SSO system, then assume request authenticated and continue with the filter chain for pre-authentication scenario.
- If request was not coming from SSO then invoke spring’s form-login and authenticate using configure authentication provide.
Second filter (SSOAuthenticationTokenPopulatorFilter)
inspects the request for the authentication mechanism
- If request is authenticated using internal customer authentication provider, then populate the SSO security tokens.
CustomFormAuthenticationFIlter:
public class CustomFormAuthenticationFilter
extends
UsernamePasswordAuthenticationFilter
{
@Override
public void
doFilter(ServletRequest req, ServletResponse res,
FilterChain
chain) throws IOException,
ServletException {
if (ssoAuthenticationTokenExists)
{
chain.doFilter(req,
res);
}
else {
super.doFilter(req, res,
chain);
}
}
}
SSOAuthenticationTokenPopulatorFilter: This filter
will populate SSO Token if form-based authentication was successful. To populate
the custom headers, we created our own custom RequestWrapper (extends HttpServletRequestWrapper), and after setting the
custom header, replaced the request in the filter chain.
public class SSOAuthenticationTokenPopulatorFilter extends
GenericFilterBean {
protected boolean isHeaderPopulationRequired()
{
Boolean customAuthentication = SecurityContextHolder.getContext()
.getAuthentication().getClass().isAssignableFrom(
UsernamePasswordAuthenticationToken.class);
return customAuthentication;
}
public void
doFilter(ServletRequest request, ServletResponse response,
FilterChain
chain) throws IOException,
ServletException {
if (isHeaderPopulationRequired
()) {
request
= addCustomHeaders (
(HttpServletRequest)
request,
(HttpServletResponse)
response);
}
chain.doFilter(request,
response);
}
public HttpServletRequest
addCustomHeaders(HttpServletRequest request, HttpServletResponse response)
{
AbstractAuthenticationToken
principal = (AbstractAuthenticationToken) request.getUserPrincipal();
request
= new CustomRequestWrapper(request);
String
sUserID = principal.getName();
((CustomRequestWrapper)
request).addCustomHeader("SSOAuthenticationToken", sUserID);
return request;
}
}
public class CustomRequestWrapper extends HttpServletRequestWrapper
{
private Map<String,
String> customHeaders = new HashMap<String, String>();
public CustomRequestWrapper (HttpServletRequest
request) {
super(request);
}
public void
addCustomHeader(String name, String value){
customHeaders.put(name, value);
}
public String getHeader(String name){
String
header = null;
if (customHeaders.containsKey(name)){
header = (String) customHeaders.get(name);
}
else {
header
= super.getHeader(name);
}
return header;
}
}
Now the most important piece, sequencing of filters:
·
customFormAutenticationhFilter
·
ssoAuthenticationTokenPopulatorFilter
·
Pre-authenticated
(RequestHeaderAuthenticationFilter)
I sequenced by
explicitly specifying before/after position for the filters, but I guess we can
directly specify it in filters attribute of intercepting url.
Sample code is available @ https://github.com/romiawasthy/ssoemulator.
Sample code is available @ https://github.com/romiawasthy/ssoemulator.
<sec:http use-expressions="true">
<sec:intercept-url pattern="/**"
access="isAuthenticated()" />
<sec:custom-filter ref="customFormAutenticationhFilter"
before="FORM_LOGIN_FILTER" />
<sec:custom-filter ref="ssoAuthenticationTokenPopulatorFilter"
after="ANONYMOUS_FILTER" />
<sec:custom-filter ref="preAuthenticatedFilter"
before="EXCEPTION_TRANSLATION_FILTER" />
<sec:form-login login-processing-url="/login_security_check"
always-use-default-target="false" authentication-failure-url="/spring_security_login?login_error"
/>
<sec:logout />
</sec:http>
Or simply
<sec:http use-expressions="true">
<sec:intercept-url pattern="/**"
access="customFormAuthenticationFilter, ssoAuthenticationTokenPopulatorFilter, preAuthenticatedFilter " />
<sec:form-login login-processing-url="/login_security_check"
always-use-default-target="false" authentication-failure-url="/spring_security_login?login_error"
/>
<sec:logout />
</sec:http>
Now you have a SSO emulator configured.