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?
The Authentication Provider Spring Security provides a variety of options for performing authentication. These options follow a simple contract; an Authentication request is processed by an AuthenticationProvider, and a fully authenticated object with full credentials is returned.
Authentication Provider calls User Details service loads the User Details and returns the Authenticated Principal. Authentication Manager returns the Authenticated Object to Authentication Filter and Authentication Filter sets the Authentication object in Security Context .
For adding a Spring Boot Security to your Spring Boot application, we need to add the Spring Boot Starter Security dependency in our build configuration file. Maven users can add the following dependency in the pom. xml file. Gradle users can add the following dependency in the build.
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.
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 :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With