In a webservice application I am currently working on, users are identified by a userId. Like any typical webservice, each request to the service needs to have the userId parameter tacked on to the request parameters. The application has a fairly sophisticated, but home-grown authentication and authorization mechanism that it inherited from the previous version of the application. Since I was using a new architecture and new libraries anyway for this application, I figured that I would give Acegi, the popular Spring based security framework, a try. I ultimately decided to not use Acegi, but I describe here the new classes and configuration I needed to build to integrate my application with Acegi, why I ended up not using it, and what parts of Acegi I decided to reimplement in my code.
The original application
The webservice is accessed via HTTP GET calls. It returns XML (or JSON) as a response. The webservice application carries out the following authentication checks. In all cases, if the check fails, a HTTP Forbidden status code of 403 with an appropriate error message is returned.
- Is the userId supplied in the request parameters? A userId request parameter must be specified in the request.
- If supplied, is it valid? We do a match against our user database to find out.
- If valid, is it coming from an IP address for which the user is registered? When the user is registered in the system, zero or more subnet masks are also registered. If no subnet masks are registered, then this authentication step is skipped. Otherwise, the user's IP address must match one of the registered subnet masks for the user.
Once these checks are done, the following authorization checks are made. As before, if the check fails, a HTTP Forbidden status code of 403 with an appropriate error message is returned.
- Is the user authorized to make the webservice method call he is making? The user is authorized to one or more webservice methods in the user database.
- Is the user authorized to any explicitly requested content? A method may return one or more response elements. If the user specifically requests one or more elements in the request, then this check is made.
- During the time the response is generated, each element is checked against the user's content authorization, and if the user is not authorized for the content, generation of the element is skipped.
The webservice is written using the Spring Framework. The first 5 checks are done by a Spring HandlerInterceptor which intercepts all method calls to the Controller object that the DispatcherServlet delegates to. If any of these fail, the response is sent to an error page with the appropriate HTTP Status code (403).
The sixth check is done in my ResponseGenerator class. The Controller delegates to a suitably configured ResponseGenerator based on the webservice method requested. The ResponseGenerator itself is simply a delegator that takes a chain of ResponseElementGenerator objects and defines a strategy for processing these objects. The chain is injected into the ResponseGenerator using Spring configuration. Because of this, the authorization code only occurs once in the application.
As you can see, the code is fairly well structured (most of the credit goes to my predecessor who structured the first 5 authentication checks in a Servlet Filter implementation in a previous non-Spring version of the application). However, I had heard a lot about Acegi, most recently from an ex-colleague who was actually integrating it into one of his client's applications, so I decided that I should at least check it out.
Changes needed for Acegi
With Acegi, I would have to configure a FilterToBeanProxy as a filter in my web.xml file, which would proxy a FilterChainProxy in my Spring application context. The FilterChainProxy would be configured with a custom Filter implementation that would extract and authenticate the user's request and use a configured custom AuthenticationManager that would actually retrieve the user by name using a custom UserDetailsService into a custom UserDetails bean. As you can see, quite a lot of custom work here. The filter declaration in the web.xml is shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <!-- web.xml -->
<web-app ...>
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
|
The supporting beans for the Acegi FilterChainProxy are defined in the Spring application context below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <beans ...>
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=requestProcessingFilter
</value>
</property>
</bean>
<bean id="requestProcessingFilter" class="com.mycompany.myapp.MyRequestProcessingFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref local="authenticationProvider"/>
</list>
</property>
</bean>
<bean id="authenticationProvider" class="com.mycompany.myapp.MyAuthenticationProvider">
<property name="userDetailsService" ref="userDetailsService"/>
<property name="userCache" ref="userCache"/>
</bean>
<bean id="userDetails" class="com.mycompany.myapp.MyUserDetails"/>
<bean id="userDetailsService" class="com.mycompany.myapp.MyUserDetailsService">
<!-- reference to a DAO to retrieve users from db -->
<property name="userDao" ref="userDao"/>
</bean>
</beans>
|
The code for the various classes referenced above are shown below. The filterChainProxy proxies the bean named requestProcessingFilter, which is in the Spring applicationContext, and which therefore can be injected with the AuthorityManager. The requestProcessingFilter pulls the userId out of the request parameters and creates a seed Authentication token with the principal set to the userId. Then it delegates to the AuthenticationProvider which returns an Authentication token populated with the UserDetails object corresponding to the userId. This is followed by a series of authentication checks (1-5).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | // MyRequestProcessingFilter.java
package com.mycompany.myapp;
public class MyRequestProcessingFilter implements Filter {
private AuthenticationProvider authenticationProvider;
@Required
public void setAuthenticationProvider(AuthenticationProvider authenticationProvider) {
this.authenticationProvider = authenticationProvider;
}
public void init(FilterConfig filterConfig) throws ServletException { /* NOOP */ }
public void destroy() { /* NOOP */ }
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String userId = ServletRequestUtils.getRequiredStringParameter(request, "userId");
Authentication authentication = new UsernamePasswordAuthenticationToken(userId, userId);
authentication = authenticationProvider.authenticate(authentication);
if (authentication.getPrincipal() instanceof UserDetails) {
// this is the only place in the code where we have access to the
// request and remote IP address, so do the additional checks here
MyUserDetails myuserdetails = (MyUserDetails) authentication.getPrincipal();
if (! myuserdetails.isAuthorizedForSubnet(request.getRemoteAddress())) {
SecurityContextHolder.getContext().setAuthentication(null);
}
if (! myuserdetails.isAuthorizedForService(getServiceFromRequest(request))) {
SecurityContextHolder.getContext().setAuthentication(null);
}
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
SecurityContextHolder.getContext().setAuthentication(null);
}
chain.doFilter(request, response);
}
}
|
The AuthenticationProvider in turn delegates to the UserDetailsService to load the user from the database. If a user is not found, then the AuthenticationProvider returns the original Authentication object passed in.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // MyAuthenticationProvider.java
public class MyAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Required
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Object principal = authentication.getPrincipal();
if (principal instanceof String) {
UserDetails userdetails = userDetailsService.loadUserByUsername((String) principal);
if (userdetails != null) {
Authentication usernamePasswordAuthToken = new UsernamePasswordAuthenticationToken(userdetails, userdetails);
usernamePasswordAuthToken.setAuthenticated(true);
return usernamePasswordAuthToken;
}
}
return authentication;
}
public boolean supports(Class authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
|
The UserDetails service delegates to an application specific UserDao that returns the UserDetails object. In reality, this returns an User object and is passed through some adapter code to convert this to a UserDetails object, but I omitted that for brevity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // MyUserDetailsService.java
public class MyUserDetailsService extends JdbcDaoSupport implements UserDetailsService {
private UserDao userDao;
@Required
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException, DataAccessException {
return userDao.getUserById(userId);
}
}
|
The MyUserDetails.java class internally stores the GrantedAuthority objects as a Map of Set of Strings, so it can be used more efficiently to look up a user's authority for a specified service or content. It also returns an array of GrantedAuthority objects as per the interface's contract. I have not included the code for this as it is long and not relevant to the discussion.
1 2 3 4 5 6 7 | // MyUserDetails.java
public class MyUserDetails implements UserDetails {
private String username;
private Map<String,Set<String>> authorities = new HashMap<String,Set<String>>();
...
}
|
For the sixth check (content level authorization), the Acegi recommended approach is to use a MethodInterceptor to wrap each ResponseElementGenerator's generate() method and use a Spring AOP Proxy to inject into the ResponseGenerator. This approach, however, would add a lot of bulk to the applicationContext.xml file, since each ResponseElementGenerator implementation (and there are quite a few) would need to be declaratively wrapped into an AOP Proxy. It is far easier and more maintainable, though perhaps not as neat, to just have this logic inside the ResponseGenerator itself, which is the current approach.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // ResponseGenerator.java
public class ResponseGenerator {
private List<IResponseElementGenerator> generators;
@Required
public void setResponseElementGenerators(List<IResponseElementGenerator> generators) {
this.generators = generators;
}
public List<? extends IResponse> generate(ParameterBean parameters, OpenSearchInfo openSearchInfo) throws Exception {
for (IResponseElementGenerator generator : generators) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// at this point, our authentication should already contain our
// UserDetails class
MyUserDetails userdetails = (MyUserDetails) authentication.getPrincipal();
if (! userdetails.isAuthorizedForContent(generator.getContentType())) {
// user not authorized for content, skip this generator
continue;
}
...
}
}
}
|
Why I ended up not using Acegi
After all this, I decided not to use Acegi after all. Although Acegi comes with a lot of built in components, none of the components seemed reusable for my application, which is why I ended up writing the custom implementations described above. At this point, I had written about as many lines of code for integrating Acegi as there were in the original security handling code in the application. Using Acegi in my application at this point would have been just for the sake of using it.
Adding Acegi features to my application
There were a few things that I saw in Acegi that I decided to implement in my application. They were the use of the static ThreadLocal class SecurityContextHolder to make the authenticated User object available throughout the application, and the built in caching of the User object.
My original application was using a reference to the UserDao inside the HandlerInterceptor as well as in the ResponseGenerator. I also needed it in some of the ResponseElementGenerator implementations, so there were lots of references to the userDao all over my applicationContext.xml file. Based on how the SecurityContextHolder works, I am now pulling in the User object in the HandlerInterceptor and putting it into a ThreadLocal called UserHolder. The ResponseGenerator, as well as the ResponseElementGenerators uses the UserHolder.get() call within the same request thread to retrieve the authenticated user from anywhere in the application.
The AbstractAuthenticationProvider class provides built in caching using EhCache. My current application had a home-grown in-memory cache of users which would get refreshed at a specific time interval, inherited from the previous version of the application. I was already using EhCache for caching webservice responses, so I decided to make my UserDao cached using EhCache with a fixed time-to-live setting.
When would I use Acegi
I think Acegi would have been a good choice if my application did not already have a security mechanism built in, or if the security mechanism it used was based on a username/password scheme, which Acegi has built-in components for. It would also have been a good choice if I was going to connect to one of the popular authentication engines that Acegi already contains connectors for. Because of the way it uses chains of AuthenticationProviders, it may also be a good choice if I was looking to change or add authentication mechanisms in the future.
7 comments (moderated to prevent spam):
Sujit Da,
I m Akash Chatterjee n I've an issue pertaining to Lucene search working on spring integrated with Hibernate runnin on JBoss 4.0.
Heres the issue -
The problem lies in searchresultscontroller.java/searchcontroller.java file under search/web/handler of an application that supports educational note sharing.
The problem is that -
When I search with query strings in different fields(as you will find in the above mentioned java files)..the keywords in resourcedto and get some files as search results.
Then I click on one of the file from within the search result and visit the file.
Here if I m logged in as an user, and the session time out is set to 1 minute in the web.xml file of the web folder not the admin folder then when I hit the BACK TO SEARCH button it easily goes back to the previous search result page along with the queries string that I had input previously.
The problem is that when I m NOT LOGGED in as an user, and I've performed a search with queries and other dropdowns in the search panel, I get the search result page, I visit the file by clicking on one of them but when I hit the BACK TO SEARCH button I don't see the previous search result page from where I had navigated to view the file.
Please suggest on what changes shall I make in the code so that even if I m not logged in as an user, I get back to the search result page on hitting the BACK TO SEARCH button from the file view page.
Solution I've thought of - Pros/Cons?-
I was thinking about calling the searchAPI and then forcing the search parameters on the previous search result page on the BACKTO SEARCH link with a query string ..so that hitting on the button I m taken back to the same result page from where I came from.
Will u pls find some time to tell me how to do it, Sujit Da?
Regards.
Akash
Hi Akash, I am guessing that your code relies on sessions (when logged in) and when you are not logged in, there is no session, hence there is no search parameters. If it is a requirement that you should be able to get back to your search results when logged out, then you may want to do what you suggested (encoding the search request params in the back to search results link url). Alternatively, you may want to use cookies.
Not sure how this pertains to Acegi though? Does your application use Acegi? If so, you may get better answers on the Acegi forum, since my experience with Acegi is limited to how to integrate it into my own app - Acegi itself is quite configurable, and it may have built in functionality to address your issue above.
Sujit Da,
You've guessed it absolutely right that it is indeed based on session.
Fact is I m trying to do the exact solution u've mentioned, but here the constraint is I just can't seem to locate where the query is generated and is held, while in session.
I've a hunch its in resourcedto.class file, but then can u pls suggest how can I implement the said solution?
No it is not using Acegi.
Sorry to've posted in the wrong post.
Its using Spring, Hibernate,Jboss4.0,JDK 1.5,Maven and the search engine used herer is Lucen.
I'll be obliged if you can suggest how to implement the solution.
By the way m Akash, working as a technical lead for IBM in New Brunswick.
Also if you could share ur email.Maybe we can strike a professional relationship and discuss some ideas, do some brainstorming.
Regards.
Akash
You are kidding, right? If yes, ha ha, sorry it took me so long to catch on. If not, my apologies, but I don't think it would make much sense for me to remote debug your app based on your description, I just don't have the bandwidth. If you want to connect up, though, comment on this blog with your email and I will reject it (for your privacy) and send you a private mail.
Nice Article.
I've a question though,
According to the bean configuration, it looks like the requestProcessingFilter has a property named authenticationManager.
But the MyRequestProcessingFilter.java don't have of such property.
But it does have the property named authenticationProvider.
Is this just a typo? or I have miss judged?
Thanks,
Mayank.
Thanks Mayank. Its been a while since I wrote this stuff, so its a bit hazy, but I quickly looked at the bean definitions in the post, and I believe you are correct - MyRequestProcessingFilter should get a reference to authenticationManager (the ProviderManager here).
What I don't get is how it worked for me...I usually just copy paste from files in my application, but it looks like I may have changed something after the fact and mistyped it. Unfortunately I no longer have the app handy to try out. My apologies for the confusion.
No problem.:)
Your article helped me alot.
Thanks,
Mayank.
Post a Comment