Persuant to my last blog post (Talking to LDAP from Grails) here is a follow-on post detailing the ridiculously easy process of using Grails' jSecurity & LDAP plugins to authenticate your application users. This example assumes your application will hand-off user authentication to LDAP and will handle authorization via filesystem or (more likely) database storage i.e. only store user IDs in your application's datastore, no passwords (yay!).
To begin:
- create a grails app (e.g. grails create-app myApp
- cd into the app's directory (e.g. cd myApp)
- install the plugins e.g.
- grails install-plugin ldap
- grails install-plugin jsecurity
- install a jSecurity LDAP realm e.g.
- grails create-ldap-realm
- install a jSecurity authenticating controller (i.e. the C in MVC, a servlet) e.g.
- grails create-auth-controller
- configure the LDAP plugin as per my previous post
As a result of the above steps you will now have the following additional/ or modified files in your Grails app's project tree:
- grails-app
- conf
- Config.groovy
- controllers
- AuthController.groovy
- realms
- JsecLdapRealm.groovy
- utils
- GldapoSchemaClassForUser.groovy
- views
- JsecLdapRealm.groovy
Now, the jSecurity script create-ldap-realm created the file JsecLdapRealm.groovy.
The line:
static authTokenClass = org.jsecurity.authc.UsernamePasswordTokensignals jSecurity that this realm will participate in authenticating users; in fact, the authenticate() method is where this work is implemented (surprise!).
By default the authenticate() method will have the typical Java/LDAP boilerplate code within it i.e.
def authenticate(authToken) {
log.info "Attempting to authenticate ${authToken.username} in LDAP realm..."
def username = authToken.username
def password = new String(authToken.password)
// Get LDAP config for application. Use defaults when no config
// is provided.
def appConfig = grailsApplication.config
def ldapUrls = appConfig.ldap.server.url ?: [ "ldap://localhost:389/" ]
def searchBase = appConfig.ldap.search.base ?: ""
def searchUser = appConfig.ldap.search.user ?: ""
def searchPass = appConfig.ldap.search.pass ?: ""
def usernameAttribute = appConfig.ldap.username.attribute ?: "uid"
def skipAuthc = appConfig.ldap.skip.authentication ?: false
def skipCredChk = appConfig.ldap.skip.credentialsCheck ?: false
def allowEmptyPass = appConfig.ldap.allowEmptyPasswords != [:] ? appConfig.ldap.allowEmptyPasswords : true
// Skip authentication ?
if (skipAuthc) {
log.info "Skipping authentication in development mode."
return username
}
// Null username is invalid
if (username == null) {
throw new AccountException("Null usernames are not allowed by this realm.")
}
// Empty username is invalid
if (username == "") {
throw new AccountException("Empty usernames are not allowed by this realm.")
}
// Allow empty passwords ?
if (!allowEmptyPass) {
// Null password is invalid
if (password == null) {
throw new CredentialsException("Null password are not allowed by this realm.")
}
// empty password is invalid
if (password == "") {
throw new CredentialsException("Empty passwords are not allowed by this realm.")
}
}
// Accept strings and GStrings for convenience, but convert to
// a list.
if (ldapUrls && !(ldapUrls instanceof Collection)) {
ldapUrls = [ ldapUrls ]
}
// Set up the configuration for the LDAP search we are about
// to do.
def env = new Hashtable()
env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.ldap.LdapCtxFactory"
if (searchUser) {
// Non-anonymous access for the search.
env[Context.SECURITY_AUTHENTICATION] = "simple"
env[Context.SECURITY_PRINCIPAL] = searchUser
env[Context.SECURITY_CREDENTIALS] = searchPass
}
// Find an LDAP server that we can connect to.
def ctx
def urlUsed = ldapUrls.find { url ->
log.info "Trying LDAP server ${url} ..."
env[Context.PROVIDER_URL] = url
// If an exception occurs, log it.
try {
ctx = new InitialDirContext(env)
return true
}
catch (NamingException e) {
log.error "Could not connect to ${url}: ${e}"
return false
}
}
if (!urlUsed) {
def msg = 'No LDAP server available.'
log.error msg
throw new org.jsecurity.authc.AuthenticationException(msg)
}
// Look up the DN for the LDAP entry that has a 'uid' value
// matching the given username.
def matchAttrs = new BasicAttributes(true)
matchAttrs.put(new BasicAttribute(usernameAttribute, username))
def result = ctx.search(searchBase, matchAttrs)
if (!result.hasMore()) {
throw new UnknownAccountException("No account found for user [${username}]")
}
// Skip credentials check ?
if (skipCredChk) {
log.info "Skipping credentials check in development mode."
return username
}
// Now connect to the LDAP server again, but this time use
// authentication with the principal associated with the given
// username.
def searchResult = result.next()
env[Context.SECURITY_AUTHENTICATION] = "simple"
env[Context.SECURITY_PRINCIPAL] = searchResult.nameInNamespace
env[Context.SECURITY_CREDENTIALS] = password
try {
new InitialDirContext(env)
return username
}
catch (AuthenticationException ex) {
log.info "Invalid password"
throw new IncorrectCredentialsException("Invalid password for user '${username}'")
}
}
Thankfully, because we're using GroovyLDAPObjects, this can be reduced to:
def authenticate(authToken) {
if (authToken.username && authToken.password) {
List matches = getEntriesByCommonName(authToken.username)
if (matches && matches.size() == 1) {
GldapoSchemaClassForUser user = getEntry(matches)
if (user.authenticate("" + authToken.password)) {
return authToken.username
} else {
java.lang.Thread.sleep(5*1000) // Wait for LDAP to reflect ACCOUNT LOCKOUT
user = getEntry(getEntriesByCommonName(authToken.username))
if ("TRUE".equals(user.lockedByIntruder)) {
throw new AccountException("The account is locked")
} else {
throw new IncorrectCredentialsException("Invalid password for user '${authToken.username}'")
}
}} else {
throw new UnknownAccountException("No account found for user [${authToken.username}]")
}
}
}
def private List getEntriesByCommonName(String id) {
return GldapoSchemaClassForUser.findAll( filter: "(cn=" + id + ")" )
}
def private GldapoSchemaClassForUser getEntry(List entries) {
if (entries && entries.size() == 1) {
return entries[0]
}
}
What the above code is doing is:
- if you passed me an ID & password then
- lookup the ID in LDAP
- iff there is 1 matching entry in LDAP then
- get that entry in the form of my GLDAPO Schema object
- try authenticating to LDAP as that user
- if authenticated then
- return the user name and we're done
- else
- wait 5 seconds
- check LDAP for the same user
- if the account is now locked out because of bad passwords then
- throw an account-locked exception
- else
- throw a login-failure exception
- if the account is now locked out because of bad passwords then
- else throw an account-not-found exception
- conf
Much briefer and easier to grok 8 months after you wrote it...
The only modification I made to the controller was to distinguish between a password failure and an account lockout so as to inform the user appropriately:
catch (AuthenticationException ex){
// Authentication failed, so display the appropriate message
// on the login page.
log.info "Authentication failure for user '${params.username}'."
if (message(code: "account.locked").contains(ex.getMessage())) {
flash.message = message(code: "account.locked")
} else {
flash.message = message(code: "login.failed")
}
and I modified the grails-app/i18n/jsecurity.properties message bundle to hold the message-to-the-user
account.locked = The account is locked, please try again later
All that remains to be done is to authorize the user e.g. do a DB check for the user ID, etc.
Finally, implement your view & business logic (only!) using jSecurity to restrict the views & business logic services to users with the appropriate roles and/or permissions. Did I blog that yet?
1 comment:
hi,
the install plugin jsecurity is failing with timeouts accessing the URLs. IS there any other way out to download this plugin to app folder?
Post a Comment