Spring Security: Custom Authenticators
In the Spring tradition, Spring Security 3 is incredibly extensible. In this tutorial I will show you how to create your own authenticators. In particular, I will build on my article “Adding Crowd Authentication to your Application” by adding Crowd support to my web application.
Atlassian used to offer a Spring Security connector for Crowd. They gave up on it when Spring Security moved to 3.0. Fortunately, it is trivial to set up the integration yourself!
Authentication
An Authentication is a central object in Spring Security. For the sake of simplicity, you can consider an authentication as a username and a password (or some other verifying credential like a certificate).
What confuses most new users of Spring Security is that an Authentication object does not necessarily represent an authenticated user. It can also represent an attempt at authentication. Access is granted to secured resources only if the Authentication object’s isAuthenticated method returns true.
AuthenticationProvider
Authentications are authenticated by AuthenticationProviders. An AuthenticationProvider checks the credentials of an authentication to ensure they are valid. Off-the-shelf implementations check database tables, LDAP registries, and other sources. Here is a very stupid example that ensures the user’s username matches his password.
public class StupidAuthenticationProvider implements AuthenticationProvider {
@Override
public boolean supports(Class<? extends Object> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
@Override
public Authentication authenticate(Authentication authentication) {
if (authentication.getName().equals(authentication.getCredentials()))
return new UsernamePasswordAuthentication(authentication.getName(), authentication.getCredentials(), null);
else
return null;
}
}
Registering the AuthenticationProvider
For this to work, we must register the AuthenticationProvider with Spring Security:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd"> <bean id='stupidAuthenticationProvider' class='StupidAuthenticationProvider'/> <security:authentication-manager> <security:authentication-provider ref='stupidAuthenticationProvider'/> </security:authentication-manager> </beans>
Crowd Integration
Now, let’s bring this together with my article on integration your application with Crowd. In this example, we check if the username and password represent an account in the Crowd registry. If they do, we grant the user various levels of administrator privileges to our web application.
public class CrowdAuthenticationProvider implements AuthenticationProvider {
@Autowired
private CrowdClient crowd;
@Override
public boolean supports(Class<? extends Object> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
@Override
public Authentication authenticate(Authentication authentication) {
try {
User user = crowd.authenticateUser(
authentication.getName(),
authentication.getCredentials().toString()
);
Administrator administrator = new Administrator();
administrator.setLastName(user.getLastName());
administrator.setLastName(user.getFirstName());
administrator.setEmail(new EmailAddress(user.getEmailAddress()));
administrator.setUsername(user.getName());
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new GrantedAuthorityImpl("ROLE_USER"));
// Optional: Define granted authorities based on
// groups to which the user is a member.
List<Group> groups = crowd.getGroupsForUser(user.getName(), 0, -1);
for (Group group: groups) {
if (group.getName().equals("..."))
authorities.add(new GrantedAuthorityImpl("ROLE_..."));
}
return new UserAuthentication(administrator, authorities);
}
catch (UserNotFoundException e) {
return null;
}
catch (InactiveAccountException e) {
throw new DisabledException(e.getMessage(), e);
}
catch (ExpiredCredentialException e) {
throw new CredentialsExpiredException(e.getMessage(), e);
}
catch (InvalidAuthenticationException e) {
throw new BadCredentialsException(e.getMessage(), e);
}
catch (ApplicationPermissionException e) {
throw new AuthenticationServiceException(e.getMessage(), e);
}
catch (OperationFailedException e) {
throw new AuthenticationServiceException(e.getMessage(), e);
}
}
}
Pay attention the exception mapping. This is pretty boilerplate; but it is critical to proper handling of authentication attempts.
Also notice that instead of creating an instance of the CrowdClient, we have Spring inject it? This allows the Crowd configuration to be extracted to a properties file. Here is how we set that up in Spring:
<context:property-placeholder location='classpath:app.properties'/>
<bean id='crowdFactory'
class='com.atlassian.crowd.integration.rest.service.factory.RestCrowdClientFactory'/>
<bean id='crowd' factory-bean="crowdFactory" factory-method="newInstance">
<constructor-arg index="0"><value>${crowd.server.url}</value></constructor-arg>
<constructor-arg index="1"><value>${crowd.application.name}</value></constructor-arg>
<constructor-arg index="2"><value>${crowd.application.password}</value></constructor-arg>
</bean>
Going Further
In some cases, only certain accounts are managed in Crowd, while others are stored elsewhere. For example, our application may allow any idiot off the street to register, while administrator accounts are managed by Crowd. Multiple AuthenticationProviders can be registered in Spring:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation=" http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd"> <bean id='crowdAuthenticationProvider' class='CrowdAuthenticationProvider'/> <bean id='databaseAuthenticationProvider' class='DatabaseAuthenticationProvider'/> <security:authentication-manager> <security:authentication-provider ref='crowdAuthenticationProvider'/> <security:authentication-provider ref='databaseAuthenticationProvider'/> </security:authentication-manager> </beans>
The AuthenticationProviders will be tried in order. The first provider to return an account or throw an exception is the one that will be used for each login attempt.
Thanks, I tweaked my implementation a bit using your example.
I am glad I was able to help!
Are these code available to download?
In the StupidAuthenticationProvider class, what should authenticate() method to return? I am new to Spring, Thanks for your help
No, I didn’t publish any code for this article. The authenticate method should return an Authentication object and not a boolean like my StupidAuthenticationProvider does (thank you for bringing this mistake to my attention, I will fix the article). Look at line 35 of the CrowdAuthenticationProvider example. You will generally return a new UserAuthentication(, ). I tried to find another example for you on the web; but was unsuccessful. Maybe I should write a more in-depth article on extended Spring Security. In the mean time, if you are new to Spring and Spring Security, you might want to take the time to read Spring in Action 3 ed. It is a fantastic book.
very nice article. To read more on spring security, you can refer to these link
Spring Security 3 – Form Login and Logout Tutorial
Spring Security 3 – MVC integration Tutorial
Thank you for the links! I would also urge anyone using Spring libraries to reference Spring’s documentation. Spring Source does a fantastic job of maintaining the best documentation in the open-source community.
For anyone interested in LDAP integration, which is provided out-of-the-box, check out this article.
Hi!
I was looking for simple tutorial on implementing custom authentication provider with current Spring Security version and yours was the most concise and containing all the info I needed.
Great job and thanks!
Thanks, I am glad I could help.
Did you also integrate SSO?
No. It is possible; but I could not find any documentation and could not justify the reverse engineering time. If you make any head way, let me know and I will update the article. Best of luck.