Reputation: 4374
I'm using Spring Security to secure a Struts2 web application. Due to project constraints, I'm using Spring Security 2.06.
My team built a custom User Management API that authenticates a user after taking in username and password parameters, and returns a custom user object containing a list of roles and other attributes like email, name, etc.
From my understanding, the typical Spring Security use-case uses a default UserDetailsService to retrieve a UserDetails object; this object will contain (among other things) a password field that will be used by the framework to authenticate the user.
In my case, I want to let our custom API do the authentication, then return a custom UserDetails object containing the roles and other attributes (email, etc).
After some research, I figured out that I can do this through a custom implementation of AuthenticationProvider. I also have custom implementations of UserDetailsService and UserDetails.
My problem is that I don't really understand what I'm supposed to be returning in CustomAuthenticationProvider. Do I use my custom UserDetailsService object here? Is that even needed? Sorry, I'm really confused.
CustomAuthenticationProvider:
public class CustomAuthenticationProvider implements AuthenticationProvider {
private Logger logger = Logger.getLogger(CustomAuthenticationProvider.class);
private UserDetailsService userDetailsService; //what am i supposed to do with this?
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
String username = String.valueOf(auth.getPrincipal());
String password = String.valueOf(auth.getCredentials());
logger.info("username:" + username);
logger.info("password:" + password);
/* what should happen here? */
return null; //what do i return?
}
@Override
public boolean supports(Class aClass) {
return true; //To indicate that this authenticationprovider can handle the auth request. since there's currently only one way of logging in, always return true
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
applicationContext-security.xml:
<beans:bean id="customUserDetailsService" scope="prototype" class="com.test.testconsole.security.CustomUserDetailsService"/>
<beans:bean id="customAuthenticationProvider" class="com.test.testconsole.security.CustomAuthenticationProvider">
<custom-authentication-provider />
<beans:property name="userDetailsService" ref="customUserDetailsService" />
</beans:bean>
To summarize, this is what I need:
Return an user entity containing roles/authorities, and other attributes like email, name, etc. I should then be able to access this object like so ..
//spring security get user name
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
userName = auth.getName(); //get logged in username
logger.info("username: " + userName);
//spring security get user role
GrantedAuthority[] authorities = auth.getAuthorities();
userRole = authorities[0].getAuthority();
logger.info("user role: " + userRole);
I hope this makes sense. Any help or pointers will be appreciated!
Thanks!
Update:
I've made some progress, I think.
I have a custom Authentication object implementing the Authentication interface:
public class CustomAuthentication implements Authentication {
String name;
GrantedAuthority[] authorities;
Object credentials;
Object details;
Object principal;
boolean authenticated;
public CustomAuthentication(String name, GrantedAuthority[] authorities, Object credentials, Object details, Object principal, boolean
authenticated){
this.name=name;
this.authorities=authorities;
this.details=details;
this.principal=principal;
this.authenticated=authenticated;
}
@Override
public GrantedAuthority[] getAuthorities() {
return new GrantedAuthority[0]; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public Object getCredentials() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public Object getDetails() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public Object getPrincipal() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public boolean isAuthenticated() {
return false; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public String getName() {
return null;
}
}
and updated my CustomerAuthenticationProvider class:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
String username = String.valueOf(auth.getPrincipal());
String password = String.valueOf(auth.getCredentials());
logger.info("username:" + username);
logger.info("password:" + password);
//no actual validation done at this time
GrantedAuthority[] authorities = new GrantedAuthorityImpl[1];
authorities[0] = new GrantedAuthorityImpl("ROLE_USER");
CustomAuthentication customAuthentication = new CustomAuthentication("TestMerchant",authorities,"details",username,password,true);
return customAuthentication;
//return new UsernamePasswordAuthenticationToken(username,password,authorities);
}
It works if I return an UsernamePasswordAuthenticationToken object, but if I try to return CustomAuthentication, I get the following error:
java.lang.ClassCastException: com.test.testconsole.security.CustomAuthentication cannot be cast to org.springframework.security.providers.UsernamePasswordAuthenticationToken
at com.test.testconsole.security.CustomAuthenticationProvider.authenticate(CustomAuthenticationProvider.java:27)
at org.springframework.security.providers.ProviderManager.doAuthentication(ProviderManager.java:188)
at org.springframework.security.AbstractAuthenticationManager.authenticate(AbstractAuthenticationManager.java:46)
at org.springframework.security.intercept.AbstractSecurityInterceptor.authenticateIfRequired(AbstractSecurityInterceptor.java:319)
at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:258)
at org.springframework.security.intercept.web.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:106)
at org.springframework.security.intercept.web.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:83)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.ui.SessionFixationProtectionFilter.doFilterHttp(SessionFixationProtectionFilter.java:67)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.ui.ExceptionTranslationFilter.doFilterHttp(ExceptionTranslationFilter.java:101)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.providers.anonymous.AnonymousProcessingFilter.doFilterHttp(AnonymousProcessingFilter.java:105)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.ui.rememberme.RememberMeProcessingFilter.doFilterHttp(RememberMeProcessingFilter.java:116)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.wrapper.SecurityContextHolderAwareRequestFilter.doFilterHttp(SecurityContextHolderAwareRequestFilter.java:91)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.ui.basicauth.BasicProcessingFilter.doFilterHttp(BasicProcessingFilter.java:174)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.ui.AbstractProcessingFilter.doFilterHttp(AbstractProcessingFilter.java:278)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.ui.logout.LogoutFilter.doFilterHttp(LogoutFilter.java:89)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.context.HttpSessionContextIntegrationFilter.doFilterHttp(HttpSessionContextIntegrationFilter.java:235)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.util.FilterChainProxy.doFilter(FilterChainProxy.java:175)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:236)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:167)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388)
at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418)
at org.mortbay.jetty.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:230)
at org.mortbay.jetty.handler.HandlerCollection.handle(HandlerCollection.java:114)
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
at org.mortbay.jetty.Server.handle(Server.java:326)
at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:536)
at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:915)
at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:539)
at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:212)
at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:405)
at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:409)
at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)
It's like something is expecting not just any Authentication object, but a specific implementation of it - UsernamePasswordAuthenticationToken. This makes me think that I may be missing another custom component .. maybe a filter?
Upvotes: 39
Views: 84264
Reputation: 140
Upvotes: 0
Reputation: 22742
If you are implementing your own AuthenticationProvider
, You don't have to implement a UserDetailsService
if you don't want to. UserDetailsService
just provides a standard DAO for loading user information and some other classes within the framework are implemented to use it.
Normally, to authenticate using a username and password, you would instantiate a DaoAuthenticationProvider
and inject that with a UserDetailsService
. That may still be your best approach. If you implement your own provider, you take on the responsibility of making sure the user has supplied the correct password and so on. However, in some cases this is a simpler approach.
To answer your "what should happen here?" comment in your code, it would be something like
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
String username = String.valueOf(auth.getPrincipal());
String password = String.valueOf(auth.getCredentials());
logger.info("username:" + username);
logger.info("password:" + password); // Don't log passwords in real app
// 1. Use the username to load the data for the user, including authorities and password.
YourUser user = ....
// 2. Check the passwords match (should use a hashed password here).
if (!user.getPassword().equals(password)) {
throw new BadCredentialsException("Bad Credentials");
}
// 3. Preferably clear the password in the user object before storing in authentication object
user.clearPassword();
// 4. Return an authenticated token, containing user data and authorities
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()) ;
}
The user object will then be accessible using the
Authentication.getPrincipal()
method, and you can access the additional properties (email etc) by casting it to your custom user implementation.
How you load the user data is up to you. All Spring Security cares about here is the AuthenticationProvider
interface.
You should also store hashed passwords and validate the supplied password using the same algorithm, rather than a simple equality check.
Upvotes: 54
Reputation: 81
thanks for posting this Luke!
Saved me from more brain damage.
Only thing of note I ran into, for anyone who cares:
My setup:
When utilizing the greatly appreciated simplified/elegant approach Luke suggests, NOT implementing a custom UserDetails (or UserDetailsService) object -and- using your own User domain object that does not extend anything special, you must take an extra step if you are using the "sec" custom tags from spring security (in your pages of course):
When you instantiate a basic, non-custom UsernamePasswordAuthenticationToken, you MUST pass it an instantiation of something that extends Principal, again, if you want your spring security custom gap tags to work. I did something like this, to keep it as simple as possible (referencing my user domain object values where useful/appropriate):
def principalUser = new org.springframework.security.core.userdetails.User(user.username, user.password, user.enabled, !user.accountExpired, !user.passwordExpired,!user.accountLocked, authorities)
def token = new UsernamePasswordAuthenticationToken(principalUser, presentedPassword, authorities)
This should satisfy the conditions tested for in grails.plugins.springsecurity.SecurityTagLib.determineSource() so, you know, your pages that use <sec:loggedInUserInfo>
will actually render:
if (principal.metaClass.respondsTo(principal, 'getDomainClass')) {
return principal.domainClass
}
Otherwise, if you instantiate the UsernamePasswordAuthenticationToken with your User domain object (as Luke show in his example), that security tag lib method (determineSource()) will just do it's level best and return the (meta) value of org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass and you'll get an error when the tag goes looking for the username member variable stating:
Error executing tag <sec:ifLoggedIn>: Error executing tag <sec:loggedInUserInfo>: No such property: username for class: org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass
Short of re-implementing/subclassing the spring-security-core plugin taglibs in my grails project, there's just no way to both use the taglibs AND use your custom domain User class to instantiate the token being passed from your filter to your provider.
Then again, one extra line of code is a very small price to pay :)
Upvotes: 3