For a variety of reasons that I'll touch on later, Roller uses Spring Security, also known as Acegi, instead of standard Java EE Servlet Authentication to implement user authentication. To explain how Roller authentication works, I'll explain how we integrated Acegi into Roller and what each part of Acegi does for us. Here's what we added when we started using Acegi in Roller:
- The Acegi jars to Roller's WEB-INF/lib directory
- An Acegi configuration at WEB-INF/security.xml
- The Acegi Servlet Filter and Spring Context listener to Roller's web.xml file
- Some code to RollerContext to do some programatic configuration of Acegi
You can check the Acegi documentation for detail about what jars to add. Let's talk about the configuration files, but first, a word about users and roles in Roller. In Roller, we have two global roles for users: 'admin' for the System Administrators who maintain the site and 'editor' for bloggers who login to blog. We use our URL structure to organize the Roller UI into sections for admin users and sections for editors. We store usernames and passwords in one database table and user roles in another. We rely on Acegi to protect the URLs we want to be protected and to authenticate users against our user tables.
What's going on in web.xml
First we add the Acegi Servlet Filter to Roller's web.xml because that's how Acegi works its magic. All of Acegi's functionality is then determined by the security.xml file described below. By default the Acegi filter is mapped to
/* which means that it applies to all requests to the application.
We also use a Context Listener so we can programmatically initialize parts of Acegi. Here's why. It's great that Acegi is so flexible and you can do so much with security.xml, but in Roller we want our users to have to edit at most one and only one configuration file to get up and running. We want that file to be a simple name and value properties file and certainly not a complicated XML file that references beans defined in some Java API. So, for example, if you want to enable or disable any of the features below, you edit properties in your roller-custom.properties file and Roller uses those property values to initialize Acegi programmatically, i.e. by calling the Acegi Java API directly.
- Remember Me: allows users to say logged-in for two weeks or until they explicitly logout
- Force Secure Login: force HTTPS for login and password editing pages, HTTP for everything else 1
- Encrypted Passwords: turn off password encryption
Those features are a benefit of using Acegi instead of plain-old Servlet Authentication. With Servlet Authentication, we'd have to implement those features in Roller and some would be impossible because there is no way to dynamically configure authentication through the Servlet API.
For most Roller setups you'll never have to touch security.xml and you can do everything through your roller-custom.properties file, but for more advanced configurations like LDAP or SSO you will probably need to understand how to deal with Acegi and security.xml.
What's going on in security.xml
To configure Acegi to protect our URLs and to authenticate against the Roller user and role tables, we create a twisty maze of nested Java beans strung together by XML in the Acegi configuration file security.xml. You'll have to understand a bit about Spring and spend some time reading the Acegi Javadocs before you can write one yourself. I'll explain what Roller's does at a high-level.
The first thing you must understand about Acegi is that it uses its own filterChainProxy to define the series of events that are going to take place as part of the Acegi authentication/authorization workflow. So to add/modify/remove any functionality you must first make adjust the filterChainProxy, which is the first bean listed in the security.xml. In that bean you will see line like this which sets the chain we use ...
Each item in the chain will be processed sequentially and be given its chance to affect the workflow. Here's what the various filters mean ...
The ChannelProcessingFilter is responsible for doing scheme enforcement. This means that if a request is supposed to be done over a specific scheme (http/https) and the current request is not using the right scheme, it will force the client to be redirected to the right scheme. We mainly use this to force https for logins and a few other things.
The HttpSessionContextIntegrationFilter is what Acegi uses to make sure a session is maintained across multiple requests. In Acegi the primary object that represents a client session is called a SessionContext and all other information about a session is available through that object. This filter is used to load/store the SessionContext object in the standard servlet HttpSession so that it is available between requests.
This filter must be early on in the chain.
The SecurityContextHolderAwareRequestFilter, which we call the remoteUserFilter, is a filter which wraps the servlet request object using a custom Acegi request wrapper so that various authentication/authorization methods of the request are properly implemented to work with Acegi. This is how request.getUserPrincipal() and request.isUserInRole() are made to work.
The AuthenticationProcessingFilter is the filter which actual checks for submitted login credentials and uses them to attempt to setup a client session by creating an Authorization object. The Authorization object is Acegi's way of indicating a client session is authentic and it holds information about authentic client.
In a nutshell what this filter does is it looks for requests posted to the filterProcessesUrl, /roller_j_security_check in our case, and for those requests it uses the submitted information to try and login the client. Much of the work is delegated to supporting beans such as the authenticationManager to do the actual authentication bit, but in the end it is this filter which makes login possible.
The RememberMeProcessingFilter is how we allow for "remembered" sessions. When this filter is reached, if Acegi does not already have a valid Authentication for the client's session then it consults any configured rememberMeServices which are given a chance to attempt an "auto-login". This is basically the same thing as an SSO login. Normally you would do this by looking for a special cookie and value which you can use to identify the client as being authentic, then use that information to create and Authentication object.
In Roller's case this is done via one of Acegi's stock remember me tools which uses a hashed cookie to mark a session that can be "remembered".
The AnonymousProcessingFilter is meant to be the last filter in the authentication phase of the Acegi workflow. All it does is checks if the client session has a valid Authentication and if not it grants a simple anonymous Authentication. This is just Acegi's way of marking the client as officially anonymous.
The ExceptionTranslationFilter is used to translate exceptions into actions. We don't really use this much, but it is used to translate a failed authentication exception from the AuthenticationProcessingFilter into an action which sends the client to the "login failed" page. So it's looking for exceptions that happened earlier in the workflow and translates them into some kind of action if it finds them.
The FilterSecurityInterceptor is the filter which handles the authorization part of the Acegi security model. To define the URLs to be protected and the roles required for each, we configure a FilterSecurityInterceptor with the rules listed below in security.xml. The rules say that most URL patterns require either the admin or editor role, but the /roller-ui/admin and /rewrite-status URLs are only for admin users.
As you would expect, this is the last filter in the chain because if a client gets past here then we assume they are authorized to access whatever they are requesting.
Acegi authentication providers
To setup Acegi to authenticate against the Roller database, we configure a Acegi ProviderManager, which has a Acegi DaoAuthenticationProvider, which has a RollerUserDetailsService, a Roller provided class that creates an Acegi UserDetails object by reading from the Roller database. That's how Acegi gets user and role information from Roller. And we configure various other beans to tell Acegi that the Roller login page is at URL /roller-ui/login.rol, the login error page is /roller-ui/login.rol?error=true and to configure Acegi's Remember Me feature.
The RollerUserDetailsService uses Roller's database layer to fetch user details. So, we don't have to ask our users to go through the tedious and error prone process of seting up a JDBC Realm as we would have had to do if we were using plain old Servlet Authentication.
What about authentication for webservices?
Authentication for Roller's remote apis and web services is handled completely separately from the Acegi authentication and authorization which controls access to the web authoring tools. Acegi has no part in webservice authentication and so all authentication to webservices is part of the api and happens via checks directly against the Roller UserManager.
1 security experts frown at Force Secure Login, but Roller site administrators want that option.