I'm doing something very similar. I'm doing authentication for a stateless REST backend, so I want the user to authenticate once, then for each subsequent request, the authentication must be transparent. I'm using tokens for this. On login, the user-supplied credentials are used to authenticate and generate a token (although ultimately, we want to use an outside service for obtaining a token). The token is returned as a header. Then the angularjs frontend sends the token on each subsequent REST call. The backend checks the validity of the token and if it's good, then marks 'authenticated' to be true.
Here's my security-context.xml:
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.2.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
<http use-expressions="true"
entry-point-ref="restAuthenticationEntryPoint"
create-session="stateless">
<intercept-url pattern="/secured/extreme/**" access="hasRole('ROLE_SUPERVISOR')"/>
<intercept-url pattern="/secured/**" access="isAuthenticated()" />
<intercept-url pattern="/j_spring_security_check" requires-channel="https" access="permitAll"/>
<intercept-url pattern="/logon.jsp" requires-channel="https" access="permitAll"/>
<sec:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
</http>
<beans:bean id="restAuthenticationEntryPoint" class="com.company.project.authentication.security.RestAuthenticationEntryPoint" />
<beans:bean id="authenticationTokenProcessingFilter" class="com.company.project.authentication.security.AuthenticationTokenProcessingFilter" >
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="userDetailsServices">
<beans:list>
<beans:ref bean="inMemoryUserDetailsService" />
<beans:ref bean="tmpUserDetailsService" />
</beans:list>
</beans:property>
</beans:bean>
<beans:bean id="tmpUserDetailsService" class="com.company.project.authentication.security.TokenUserDetailsServiceImpl" />
<user-service id="inMemoryUserDetailsService">
<user name="temporary" password="temporary" authorities="ROLE_SUPERVISOR" />
<user name="user" password="userPass" authorities="ROLE_USER" />
</user-service>
<authentication-manager alias="authenticationManager">
<!-- Use some hard-coded values for development -->
<authentication-provider user-service-ref="inMemoryUserDetailsService" />
<authentication-provider ref='companyLdapProvider' />
</authentication-manager>
For the authentication filter, I subclass UsernamePasswordAuthenticationFilter. When it's a login request, then authentication with the authentication provider happens, and then a token is generated. If a token is read from the header, then the token is examined for authentication. Here is my authentication filter (which is still not production-ready, but it works to give you an idea of what you can do):
public class AuthenticationTokenProcessingFilter extends UsernamePasswordAuthenticationFilter {
//~ Static fields/initializers =====================================================================================
private static final String HEADER_AUTH_TOKEN = "X-Auth-Token";
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationTokenProcessingFilter.class);
private List<UserDetailsService> userDetailsServices = new ArrayList<UserDetailsService>();
//~ Constructors ===================================================================================================
public AuthenticationTokenProcessingFilter() {
super();
}
//~ Methods ========================================================================================================
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String authToken = this.extractAuthTokenFromRequest(request);
if (authToken == null) {
super.doFilter(request, res, chain);
return;
}
String userName = TokenUtils.getUserNameFromToken(authToken);
if (userName != null) {
UserDetails userDetails = loadUserByUsername(userName);
if (TokenUtils.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
UsernamePasswordAuthenticationToken authRequest = authenticateWithForm(request, response);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
Authentication authentication = this.getAuthenticationManager().authenticate(authRequest);
if (authentication.isAuthenticated()) {
try {
String authToken = TokenUtils.createToken(obtainUsername(request), obtainPassword(request));
LOGGER.info("Setting HTTP header {} = {}", HEADER_AUTH_TOKEN, authToken);
response.addHeader(HEADER_AUTH_TOKEN, authToken);
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
LOGGER.info("authorities = {}", authorities);
// Now we should make an in-memory table of the token and userdetails for later use
} catch(Exception e) {
LOGGER.warn("Error creating token for authentication. Authorization token head cannot be created.", e);
}
}
return authentication;
}
protected UsernamePasswordAuthenticationToken authenticateWithForm(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return authRequest;
}
private String extractAuthTokenFromRequest(HttpServletRequest httpRequest) {
/* Get token from header */
String authToken = httpRequest.getHeader(HEADER_AUTH_TOKEN);
/* If token not found get it from request parameter */
if (authToken == null) {
authToken = httpRequest.getParameter("token");
}
return authToken;
}
public List<UserDetailsService> getUserDetailsServices() {
return userDetailsServices;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsServices.add(userDetailsService);
}
public void setUserDetailsServices(List<UserDetailsService> users) {
if (users != null) {
this.userDetailsServices.clear();
this.userDetailsServices.addAll(users);
}
}
private UserDetails loadUserByUsername(String username) {
UserDetails user = null;
List<Exception> exceptions = new ArrayList<Exception>();
for (UserDetailsService service: userDetailsServices) {
try {
user = service.loadUserByUsername(username);
break;
} catch (Exception e) {
LOGGER.warn("Could not load user by username {} with service {}", username, service.getClass().getName());
LOGGER.info("Exception is: ",e);
exceptions.add(e);
}
}
if (user == null && !exceptions.isEmpty()) {
throw new AuthenticationException(exceptions.get(0));
}
return user;
}
}
I'm still working on getting the UserDetailsService refined though. Usually, you can use the authentication provider to get the UserDetails, but since I have a stateless app, when I want to authenticate a token, I have to figure which UserDetailsService to use. I'm doing this with custom code for the moment.