Thursday, October 29, 2009

Using 2 Grails plugins to authenticate your applications' users

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.UsernamePasswordToken

    signals 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


      • else throw an account-not-found exception


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:

Unknown said...

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?