This is my first installment in the What Grooves You? series of posts, and it deals with the first thing you’re going to need to consider if you are deploying your Grails/Groovy applications in the average corporate IT infrastructure, Single Sign On with Active Directory and NTLM. Like it or not, because all of our existing applications are based on Microsoft technologies our users have gotten used to just going to the URL for the application they intend to use and being instantly recognized and authenticated. Forcing them to sign in again, or worse still forcing them to setup a new username and password for your system would be completely unacceptable! Below, I’ll take you through the steps I took to solve this problem, including the detours that cost me time!
Throughout this post I’ll be referring to domain objects, controllers, and views which would have been created by running the Acegi LDAP tutorial. So if you want to follow along, go walk through the tutorial, then come back here to see how we tweak it. Be sure to take the “optional” step of creating the views and controllers for the auth domains, you’ll need it not only in the tutorial, but also for some customization we’ll be doing later.
grails generate-manager
You can also download a copy of the Spring Source STS project I used for this application here.
LDAP Single Identity but not Single Sign On
As I started searching to figure out how to authenticate my users, everything seemed to point to using the Acegi Plugin for Grails, and using it with LDAP, tweaked to talk to Active Directory. So, I started down this path and followed the LDAP tutorial for the Acegi plugin. Without a lot of trouble I got this working by following the steps in the tutorial, though I did make a couple significant changes to the SecurityConfig.groovy file to make it play nice with Active Directory
SecurityConfig.groovy
security {
// see DefaultSecurityConfig.groovy for all settable/overridable properties
active = true
useLdap = false
ldapRetrieveDatabaseRoles = false
ldapRetrieveGroupRoles = true
ldapServer = 'ldap://<your-domain-controller>'
ldapManagerDn = '<your-service-account-dn>'
ldapManagerPassword = '<your-service-account-password>'
ldapSearchBase = 'OU=People,DC=yourdomain,DC=com'
ldapSearchFilter = '(sAMAccountName={0})'
ldapSearchSubtree = true
ldapGroupSearchBase = 'CN=Users,DC=yourdomain,DC=com'
ldapGroupSearchFilter = 'member={0}'
ldapUsePassword = false
loginUserDomainClass = "User"
authorityDomainClass = "Role"
requestMapClass = "Requestmap"
}
By setting ldapSearchFilter to ‘(sAMAccountName={0})’, we’re telling the Acegi plugin to look for the field in Active Directory that stores the username, this means that users can login just like they’re used to by using their Active Directory user name and password.
The ldapGroupSearchBase and ldapGroupSearchFilter are set such that any domain user groups that a user is a member of become Acegi “Roles” which can be used to determine if a user has authority to do stuff in the application.
Setting ldapUsePassword to false is important too. What we’re telling the Acegi plugin is not to extract the users password from Active Directory. If you don’t set this to false, you’ll get a lovely exception which isn’t particularly useful, java.lang.IllegalArgumentException: Cannot pass null or empty values to constructor. What this is trying to tell you is that the users password is null, which is correct since the default setting for the Acegi plugin is to try to extract the users password from Active Directory, and we haven’t told Acegi what attribute Active Directory stores the password in. By setting ldapUsePassword to false, the plugin provides a bogus password for the user details, and we’re able to proceed without incident.
So, great! If you’ve made these changes and followed the steps in the tutorial to add a user to the application domain with the same username as your Active Directory user you can authenticate users with the username and password they’re already using to login to their computer, we have a single identity for this person. But remember, our users are used to simply going to a URL and not being prompted to login. How do we reproduce that experience?
NTLM Really Single Sign On
Having realized that using Active Directory/LDAP authentication works, but isn’t an actual single sign on solution I started looking into the other settings in the SecurityConfig.groovy file. Turns out there is an NTLM option, but no corresponding tutorial! Here’s my configuration for using NTLM.
SecurityConfig.groovy
security {
// see DefaultSecurityConfig.groovy for all settable/overridable properties
active = true
useNtlm = true
ntlm.stripDomain = false
ntlm.defaultDomain = "yourdomain.com"
ntlm.netbiosWINS = "<your-domain-controller-ip>"
loginUserDomainClass = "User"
authorityDomainClass = "Role"
requestMapClass = "Requestmap"
}
Nothing really outstanding here, I chose to set ntlm.stripDomain to false, so that the users name is not prefixed by the domain, I.E. DOMAIN\username. Also ntlm.defaultDomain and ntlm.netbiosWINS are both required, and I found that the ntlm.netbiosWINS works better if you actually give it the IP address of your domain controller, rather than the DNS name.
With NTLM configured, our Grails app now accepts the users cached authentication from their windows session. True single sign on!
Securing the Security Controllers and BootStrapping
Now we’ve got an application which will use NTLM to authenticate the user, we restrict access using the requestmap, and we add users and their roles using the user and role controllers. The problem, of course is that if you deploy this application anyone can go right to http://yourdomain.com/sso-app/user and add themselves as a user with any roles they see fit! So we need to make sure that our security controllers are secured, and that there is an authorized user that can get in to add users and roles.
Securing the Security Controllers
The first thing we’re going to want to do is secure our authentication controllers. My preference is to do this with annotations on the controllers, so lets secure the UserController and RoleController with annotations. Shown below we annotate them to show that the “ROLE_USER_ADMINISTRATOR” role is necessary to access any of the pages for either controller.
UserController.groovy
import org.codehaus.groovy.grails.plugins.springsecurity.Secured;
@Secured(["ROLE_USER_ADMINISTRATOR"])
/**
* User controller.
*/
class UserController {
/* Snip */
}
RoleController.groovy
import org.codehaus.groovy.grails.plugins.springsecurity.Secured;
@Secured(["ROLE_USER_ADMINISTRATOR"])
/**
* Authority Controller.
*/
class RoleController {
/* Snip */
}
Enabling the @Secured annotation
Now if you were simply to run the application like this, you’d find that you can still freely navigate to /user and /role without having to be authenticated, that’s because we are still configured to use the Requestmap to restrict access to specific parts of our application. To configure this for use with annotations instead, we only need to make a couple of small changes to the SecurityConfig.groovy file.
SecurityConfig.groovy
security {
// see DefaultSecurityConfig.groovy for all settable/overridable properties
active = true
useNtlm = true
ntlm.stripDomain = false
ntlm.defaultDomain = "yourdomain.com"
ntlm.netbiosWINS = "<your-domain-controller-ip>"
loginUserDomainClass = "User"
authorityDomainClass = "Role"
//requestMapClass = "Requestmap"
userRequestMapDomainClass = false
useControllerAnnotations = true
}
Setting useControllerAnnotations to true gives the @Secured annotations on the user and role controllers meaning. By setting userRequestMapDomainClass to false we tell the Acegi security plugin to not even query for restrictions in the database, but to use only the static configuration defined by the annotations. We also delete the requestMapClass line. I commented it here instead of deleting it so it can be highlighted as a change in the file, but we’re not quite done with the requestmap yet. Now that we’ve configured our application not to use the request map we can delete the controller, domain, and views for it. Go ahead and delete the following files.
- /grails-app/controller/RequestmapController.groovy
- /grails-app/domain/Requestmap.groovy
- /grails-app/views/requestmap/*
BootStrapping
Now that we have our app secured, and we’re using annotations to restrict access we need to make sure that some user can successfully login after we first deploy our application. Otherwise no one will be able to access the pages to add new users! So we use boot strapping to add a user administrator who can add more users and roles for the application.
BootStrap.groovy
class BootStrap {
def authenticateService
def init = { servletContext ->
def role = new Role(authority: 'ROLE_USER_ADMINISTRATOR', description:'User Administrator')
role.save()
def user = new User(username: 'admin',
userRealName: 'Administrator',
passwd: authenticateService.encodePassword('foobar'),
enabled: true,
description: '',
email: '',
emailShow: false)
user.addToAuthorities(role)
user.save()
}
def destroy = {
}
}
So there you have it, an application which allows users to connect using their cached authentication using NTLM, uses annotations to secure your controllers, and actually allows access to at least one user once it’s deployed. Now, there are still some weaknesses. Particularly the level of permissions you can assign is limited to roles, I.E. “Administrator”, “User”, “Reporting User”, “User Administrator”, etc. Also in a corporate environment, having to go through this configuration for every grails app, and adding users and assigning their roles for each app can be tedious. I’ll be looking into improving these things with Acegi ACL’s and possibly using a centralized database for users and roles in future articles so stay tuned!
#1 by Scott Ellis on February 10, 2010 - 4:05 pm
Excellent article, Ryan. Crystal clear, very nice solution.
#2 by Martin Flower on February 11, 2010 - 1:43 am
Hi Ryan. Thanks for the detailed write-up. One problem we have is that the user’s session times out after 30 minutes of inactivity (Tomcat setting). When trying to access a page a 500 is returned. When refreshing the page all is fine. Have you encountered this problem – would you know how to resolve it ?
Thanks
Martin
#3 by RyanG on February 16, 2010 - 9:57 am
Martin, we have not yet experienced an issue with inactivity, since we’re still in the early phases of implementing.
Are you using NTLM authentication, or one of the other Acegi authentication mechanisms?
I would expect that unless you’re using a whole lot of javascript you’d be doing server roundtrips fairly frequently, and if the authentication did become stale, the browser and server should negotiate again. Perhaps I’ll do some testing to see if I can reproduce!
#4 by Waseem Bashir on February 23, 2010 - 10:28 am
Thanks for the post, was exactly looking for this. Will implement it soon and will let you know.
Keep posting.
Regards,
WB
#5 by Waseem Bashir on February 24, 2010 - 11:01 am
Hi Ryan,
I have created my Grails app in IntelliJ. I followed your above snippet. However, when I run my app and login using my login details I get the following error:
HTTP Status 404 – /activeDirectories/j_spring_security_check
type Status report
message /activeDirectories/j_spring_security_check
description The requested resource (/activeDirectories/j_spring_security_check) is not available.
Apache Tomcat/6.0-snapshot
Please can you tell me where I am going wrong?
Much appreciated,
Regards,
Waseem
#6 by Waseem Bashir on February 25, 2010 - 8:14 am
a noob question Why have you got useLdap = false at the top?
#7 by RyanG on February 25, 2010 - 9:30 am
Waseem, the project I included has all the code from both approaches. Both LDAP and NTLM. However since this writeup ended using NTLM authentication, LDAP is disabled, I don’t expect they would play nice together.
Did you resolve your 404 error?
#8 by Waseem Bashir on February 26, 2010 - 2:34 am
Ryan,
Thanks for the reply, yes I have resolved the 404 error but now stuck in another issue.
I am unable to configure my security.groovy file. With no matter different settings I try I am unable to login, it says incorrect username/password and there are no errors logged.
Well can you please help me configure my ldapSearchFilter and my ldapGroupSearchFilter.
Currently I have the followings settings:
ldapSearchBase = ‘ou=Users,ou=London,ou=EMEA,dc=global,dc=somecompany,dc=com’
ldapSearchFilter = ‘(sAMAccountName={0})’
//As in our LDAP users directory we too have an attribute sAMAccountName
and for my group search base I am using:
ldapGroupSearchBase = ‘cn=Groups,ou=London,ou=EMEA,dc=global,dc=somecompany,dc=com’
ldapGroupSearchFilter = ‘member={0}’
//Please note that one user can be a member of many groups.
Also my Users directory contains a memberOf attribute.
Please let me know when you get the time.
Thanks a lot.
Waseem.
p.s. Is there any way I can see any errors thrown back currently Im just in the dark and also Please throw some light on the ‘member{0}’ filter you used.
#9 by Waseem Bashir on February 26, 2010 - 3:24 am
This might be more helpful:
LDAP structure
dc=global, dc=myCompany,dc=com
ou=EMEA
ou=London
ou=Users
cn=Waseem Bashir
dc=global, dc=myCompany,dc=com
ou=EMEA
ou=London
ou=Groups
cn=London Group
dn=member
cn=Waseem Bashir
I create a role:
ROLE_LONDON_GROUP in my acegi roles. But still no luck.
Let me know please.
#10 by Waseem Bashir on February 26, 2010 - 4:42 am
Hi Ryan,
I solved the case, had some typos in my attributes.
Thanks for putting up this post in the first place.
Please keep posting (especially) on grails.
Regards,
Waseem
#11 by Waseem Bashir on February 26, 2010 - 7:42 am
One query though
if I have a group London Staff in my LDAP.
What should be the syntax for my role in Acegi?
I have tried:
ROLE_LONDON_STAFF
ROLE_LONDON STAFF
ROLE_LONDONSTAFF
Please let me know
Thanks.
#12 by RyanG on February 27, 2010 - 3:14 pm
The proper format is ROLE_ where is the exact name of the group from your LDAP configuration, including spaces, case sensitivity and punctuation.
So your role would be ROLE_London Staff
#13 by Matt Passell on February 28, 2010 - 11:16 am
Hi Ryan. Nice post.
I noticed an unfortunate typo in your title: “NTML” –> “NTLM”. If it’s possible to change the title without changing the permalink (since there are probably some references to the existing URL), that might help people find it in their Google searches.
–Matt
#14 by RyanG on February 28, 2010 - 12:39 pm
Oops! Thanks for that catch Matt. Changed the title, and permalink remains the same. Yikes, that’s embarrassing!
#15 by Waseem Bashir on March 1, 2010 - 7:38 am
yeah worked thanks,
ROLE_London Group
I am now interested in using SSL. Any interesting read?
or any posts coming up on SSL’s in grails.
#16 by RyanG on March 2, 2010 - 9:28 am
Waseem, what exactly are you trying to accomplish with SSL in Grails? There isn’t any particular configuration necessary to use a Grails application behind SSL.
If you’re referring to redirecting to an SSL encrypted version of a page, I’d recommend using Apache mod_rewrite.
#17 by Waseem Bashir on March 5, 2010 - 4:06 am
Hi Ryan,
Thanks for your reply, sorry let me repharase my question:
I have installed SSL on my server. My question is how can i forcehttps only on selected controllers/pages via the acegi plugin.
Acegi plugin supports a property forcehttps, which when set to true makes all the pages secured once the user logs in. I want to change this behaviour where once the users log off, they should be redirected to the unsecure page.
So in essence http>https>http.
Hope I am clear.
Thanks for your help.
WB
#18 by Waseem Bashir on March 5, 2010 - 9:29 am
Hi Ryan,
I just got an another curveball.
In our organization we have two categories of users:
1) internal
2) External
Internal users are stored as:
dc=global, dc=myCompany,dc=com
ou=EMEA
ou=London
ou=Users
cn=Waseem Bashir
However, external ones are stored as:
dc=global, dc=myCompany,dc=com
ou=EMEA
ou=London
ou=External Users
cn=ExternalUser1
Is it possible to tell the Acegi plugin to look for users that are stored in them.
Thanks,
Hoping to hear from you soon on both the queries.
#19 by RyanG on March 9, 2010 - 10:09 am
Waseem,
I’m afraid I don’t have any experience with the SSL scenario you’ve described, so I’m not going to be of much assistance there.
As for the issue of the two sets of users. You should be able to set your ldapSearchBase to ‘dc=global, dc=myCompany, dc=com, ou=EMEA, ou=London’ and set the ldapSearchSubtree to true, and it will find any users at that branch of the LDAP hierarchy, or below. You can go up as high in the tree as you like also, like up to ‘dc=global,dc=myCompany,dc=com’. Then any LDAP record with an attribute that matches the LDAP query you have in ldapSearchFilter will be found and treated as a user.
#20 by Waseem Bashir on March 10, 2010 - 2:24 am
Hi RyanG,
Thanks for your help
indeed ldapSearchSubtree = true, this is what I wanted.
Thanks a ton.
Have some questions coming up for you in the pipeline.