“Hacking the Grails Spring Security Plugin” at Groovy & Grails Exchange
Friday, December 09th, 2011I gave a talk at the Groovy & Grails Exchange in London called “Hacking the Grails Spring Security Plugin”. I didn’t want to spend a lot of time discussing the sample app code since there was a lot of material to cover, so I’m making the code available here with a brief discussion of the implementation.
To support a custom login where the user’s organization must be specified in addition to the standard username and password, there’s a custom AuthenticationProvider
(hacking.extralogin.auth.OrganizationAuthenticationProvider
) which processes a subclass of UsernamePasswordAuthenticationToken
(hacking.extralogin.OrganizationAuthentication
) that adds an organizationName
property, and a filter (hacking.extralogin.ui.OrganizationFilter
) that creates the authentication from the request and initiates authentication.
In this example all authentication uses this approach, so the filter replaces the standard "authenticationProcessingFilter"
bean (and subclasses the plugin’s RequestHolderAuthenticationFilter
to maintain its functionality) and the provider replaces the "daoAuthenticationProvider"
bean. The provider directly implements the AuthenticationProvider
interface since using GORM directly is simple enough to not need to delegate to a UserDetailsService
or other helper classes.
You can see the bean registrations for the filter and provider in grails-app/conf/spring/resources.groovy
.
Note that the auth provider and filter are in separate packages to reinforce the idea that auth providers shouldn’t be aware of the UI. The filters that call the auth providers create an Authentication
instance with all of the information that’s needed to authenticate, getting most or all of the data from the HTTP request. This keeps the auth providers modular and reusable outside of a web application.
No changes are required for the generated User
, Role
, or UserRole
classes, but a new domain class Organization
is needed to store the organization names, and OrgUser
is needed to provide a link between users and organizations. auth.gsp
has an extra input, a <select>
box with all available Organization
names.
If you request http://localhost:8080/hacking_london/secure you should see the text “not secured” since the index action is not guarded. But navigating to http://localhost:8080/hacking_london/secure/admin requires a user with ROLE_ADMIN
, http://localhost:8080/hacking_london/secure/user requires a user with ROLE_USER
, and http://localhost:8080/hacking_london/secure/adminOrUser requires a user with either ROLE_ADMIN
or ROLE_USER
. You can use one of the users created in BootStrap.groovy
:
username | Organization name | password | Role |
---|---|---|---|
admin | Org1 | password | ROLE_ADMIN |
user | Org2 | password | ROLE_USER |
disabled | Org1 | password | ROLE_USER |
There’s an extra user (“disabled”) with a disabled account to test that login fails with a correct username, org name, and password.
We also need to tweak an error message. The plugin’s i18n message bundle will display the error “Sorry, we were not able to find a user with that username and password” if the username, password, or organization are wrong. But we should include the organization in the message to indicate that it might have been wrong. To fix this, add this line to your application’s grails-app/i18n/messages.properties
: springSecurity.errors.login.fail=Sorry, we were not able to find a user with that username, organization, and password.
To test this, log in as user ‘user’ with password ‘password’ but leave the organization name selected as ‘Org1’.
Note that since we’re not using the plugin’s UserDetailsService
or Spring Security’s DaoAuthenticationProvider
we don’t need the grails.plugins.springsecurity.userLookup.userDomainClassName
, grails.plugins.springsecurity.userLookup.authorityJoinClassName
, or
grails.plugins.springsecurity.authority.className
properties added to Config.groovy
by the s2-quickstart
script. They’re commented out so you can switch back to the standard authentication approach by removing or commenting out the bean overrides in resources.groovy
and the organization select box in auth.gsp
.
The other significant customization I discussed was doing a custom post-logout redirect. It is possible to specify a spring-security-redirect
request parameter when logging out, but this is too coarse an approach in general. If you need to use logic specific to the user, or something about the current authentication state, you need more control. So the sample application subclasses the default implementation of LogoutSuccessHandler
, SimpleUrlLogoutSuccessHandler
with hacking.logout.CustomLogoutSuccessHandler
and registers it as the logoutSuccessHandler
bean in resources.groovy
.
The logic is contrived; if you’re in Organization
‘Org1’ you’re redirected to ‘http://yahoo.com’ and if you’re in Organization
‘Org2’ you’re redirected to ‘http://google.com’. Otherwise you’re redirected to the default location (‘/’ unless you’ve customized it with the grails.plugins.springsecurity.successHandler.defaultTargetUrl
config attribute). But it shows an example of how you could use your own business logic to make a similar decision.
There’s one wrinkle here though; the only parameters for the overridden determineTargetUrl
method are the HttpServletRequest
and HttpServletResponse
, but not the Authentication
. And since this is the last step of the logout process, the user has already been logged out and the Authentication
isn’t available from the request, springSecurityService
, SecurityContextHolder
, etc. But the public method (onLogoutSuccess
) that calls this method has a parameter for the Authentication
, so we save it in a ThreadLocal
so it’s available for our override.
You can get the PDF of the presentation here, and the zip of the sample project here.