views:

470

answers:

2

I'm using the Acegi Security plugin for Grails, and authentication via LDAP.

The application logs show that on login, we can authenticate the user and get their roles via LDAP, but the login fails because the User Details cannot be found in the application's database.

Is there a way to auto create and save a basic User Details domain object if one doesn't already exist?

-- Update
Here are the relevant debug entries I am currently seeing

DEBUG populator.DefaultLdapAuthoritiesPopulator  - Roles from search: [Role1, Role2, etc]
ERROR springsecurity.GrailsDaoImpl  - User not found: MyUserName
DEBUG rememberme.TokenBasedRememberMeServices  - Interactive login attempt was unsuccessful.
+1  A: 

Sure. You need to implement custom AuthProvider

SecurityConfig.groovy:

security {
   providerNames = ['ldapAuthProvider']
}

Ldap Auth Provider:

import domain.user.AppUser
import org.apache.commons.codec.digest.DigestUtils
import org.apache.log4j.Logger
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUserImpl
import org.springframework.security.BadCredentialsException
import org.springframework.security.GrantedAuthority
import org.springframework.security.GrantedAuthorityImpl
import org.springframework.security.providers.UsernamePasswordAuthenticationToken
import org.springframework.security.providers.dao.AbstractUserDetailsAuthenticationProvider
import org.springframework.security.userdetails.UserDetails

/**
 * Authentication provider that checks user credentials against LDAP
 */
class LdapAuthProvider extends AbstractUserDetailsAuthenticationProvider {
    private static final Logger log = Logger.getLogger(LdapAuthProvider.class)

    def appUserService

    /**
     * Checks password hash stored in the session with password in authentication token.
     */
    protected void additionalAuthenticationChecks(UserDetails details,
        UsernamePasswordAuthenticationToken authentication) {

        if (details.password != DigestUtils.md5Hex(authentication.credentials)) {
            throw new BadCredentialsException(details.username)
        }
    }

    /**
     * Retrieves user from LDAP,
     * checks credentials,
     * updates local copy of user data,
     * returns user details.
     */
    protected UserDetails retrieveUser(String login, UsernamePasswordAuthenticationToken authentication) {
        AppUser.withTransaction {
            log.debug("Trying to retrieve user \"$login\"...")
            def password = authentication.credentials?.toString()

            def ldapUser = appUserService.findLdapUser(login)
            if (!(password && ldapUser?.authenticate(password))) {
                log.debug("Can't authenticate \"$login\"")
                throw new BadCredentialsException(login)
            }

            AppUser localUser = AppUser.findByLogin(login, [cache: true])

            if (!localUser) {
                log.debug("Can't authenticate \"$login\"")
                localUser = appUserService.updateLocalUser(ldapUser)
            }

            log.debug("User \"$login\" is authenticated.")
            def authorities = localUser.collectAuthorities().collect {String authority ->
                log.debug("\thas right \"$authority\"")
                new GrantedAuthorityImpl(authority)
            }

            def userDetails = new AppUser();
            userDetails.setAssignedTemplate(localUser.assignedTemplate)
            userDetails.setFullName(localUser.getFullName())
            userDetails.setLogin(localUser.getLogin())
            userDetails.setEmail(localUser.getEmail())
            userDetails.setDisabled(localUser.getDisabled())
            userDetails.setManager(localUser.getManager())
            userDetails.setRoles(new HashSet(localUser.getRoles()))


            log.debug("Retrieving user \"$login\" is completed.")
            return new GrailsUserImpl(userDetails.login, DigestUtils.md5Hex(password), true, true, true, true,
                authorities.toArray(new GrantedAuthority[authorities.size()]), userDetails)
        }
    }
}

And in appUserService.updateLocalUser(ldapUser) you need create/modify your Domain object and persist in database.

AppUser updateLocalUser(LdapUser ldapUser) {
    def login = ldapUser.login
    log.debug("Start updating local user ${login}...")
    def localUser = AppUser.findByLogin(login, [cache: true]) ?: new AppUser()
    if (localUser.id) {
        log.debug("user $login was found in local DB")
        if (localUser.disabled ^ ldapUser.isDisabled()) {
            log.debug("...user ${login} has been ${localUser.disabled ? 'activated' : 'disabled'}...")
        }
    } else {
        log.debug("user $login is new")
    }
    localUser.login = login
    localUser.email = ldapUser.email
    localUser.fullName = ldapUser.fullName ?: login
    localUser.disabled = ldapUser.isDisabled();
    localUser.roles?.clear()
    ldapUser.memberOf.collect { Role.findByLdapName(it, [cache: true]) }.each {role ->
        if (role) {
            localUser.addToRoles(role)
        }
    };
    localUser.save(flush: true)
    log.debug("Update local user $login is complete.")
}

UPDATE #1

You can implement custom UserDetailsService:

package com.foo.bar;

import org.springframework.security.userdetails.UserDetails; import org.springframework.security.userdetails.UserDetailsService;

public class MyUserDetailsService implements UserDetailsService {

public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException, DataAccessException {

// lookup user and data

return new MyUserDetails(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities, id, fullName); } }
uthark
Do I really need to implement the custom LdapProvider? Using the standard one in Spring-Security it seems I can get the relevant user/role data out of the LDAP store. Seem's I just need to stick in a custom UserDetailsService or something to skip the part where it looks in the database for the user object. I've included some of the debugging statements in the question
Benny Hallett
Code above is from actual project with some cleaups. I've thought about your comment and think you can use custom UserDetailsService. I've updated answer.
uthark
Yeah I got it sorted with a Custom UserDetailsService. The only thing is that there is a second loadUserByUsername method, that takes the username and a bool which represents whether to look up Roles from the database. In my case, the second method was getting called, and I needed to override that one.
Benny Hallett
A: 

Forgive my ignorance but I learned a long time ago to ask and look stupid than hold my tongue and "be" stupid. :)

In the solution by uthark it notes to modify appUserService.updateLocalUser(). I've not been able to find a reference to this service. I hope to understand as this seems the perfect way to ensure the local user's attributes (name, email, etc.) are synced to what's in the LDAP.

Thank you in advance!

  • Matt

EDIT: I've figured the above out, but now I'm having trouble wiring the new LdapAuthProvider in. Any hints?

Matt