Question

I have a problem to get user authentication running in a Grails application with spring-security and LDAP.

The connection to LDAP works fine, I get results. But I didn't get it managed that the the user can log in and that the data is saved in the local database.

I have changed/created the following files:

config.groovy

grails.plugin.springsecurity.ldap. context.managerDn = 'USERNAME'
grails.plugin.springsecurity.ldap. context.managerPassword = 'PASSWORD'
grails.plugin.springsecurity.ldap. context.server ='ldap://LDAPSERVER:389/'
grails.plugin.springsecurity.ldap. authorities.ignorePartialResultException = true // typically needed for Active Directory
grails.plugin.springsecurity.ldap. search.base = 'DC=example,DC=com'
grails.plugin.springsecurity.ldap. search.filter='(sAMAccountName={0})' // for Active Directory you need this
grails.plugin.springsecurity.ldap. search.searchSubtree = true
grails.plugin.springsecurity.ldap.authorities.groupSearchBase ='DC=example,DC=com'
grails.plugin.springsecurity.ldap.authorities.groupSearchFilter = 'member={0}'
grails.plugin.springsecurity.ldap.authorities.retrieveDatabaseRoles = false
grails.plugin.springsecurity.ldap. auth.hideUserNotFoundExceptions = false
grails.plugin.springsecurity.ldap. search.attributesToReturn = ['mail', 'displayName', 'title', 'fullname'] // extra attributes you want returned; see below for custom classes that access this data
grails.plugin.springsecurity.providerNames = ['ldapAuthProvider']
grails.plugin.springsecurity.ldap.useRememberMe = false
grails.plugin.springsecurity.ldap.authorities.defaultRole = 'ROLE_USER'
grails.plugin.springsecurity.ldap.mapper.userDetailsClass = 'CustomUserDetails'

src/grovvy/CustomUserDetailsMapper.grovvy

package com.example


import com.example.CustomUserDetails
import org.springframework.ldap.core.DirContextAdapter
import org.springframework.ldap.core.DirContextOperations
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper

import groovy.sql.Sql

import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.GrantedAuthority


import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.authentication.DisabledException

class CustomUserDetailsContextMapper implements UserDetailsContextMapper {

    private static final List NO_ROLES = [new SimpleGrantedAuthority("ROLE_USER")]

    def dataSource

    @Override
    public CustomUserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<GrantedAuthority> authority) {

        username = username.toLowerCase()

        User.withTransaction {

        User user = User.findByUsername(username)

        String firstName = ctx.originalAttrs.attrs['givenname'].values[0]
        String lastName = ctx.originalAttrs.attrs['sn'].values[0]


        def roles



            if(!user){
                user = new User(username: username, enabled: true, firstName: firstName, lastName: lastName)
                user.save(flush: true)
            }
            else {
                user = User.findByUsername(username)
                user.firstName = firstName
                user.lastName = lastName
                user.save(flush)
            }

            roles = user.getAuthorities()
        }

        if( !user.enabled )
            throw new DisabledException("User is disabled", username)


        def authorities = roles.collect { new SimpleGrantedAuthority(it.authority) }
        authorities.addAll(authority)
        def userDetails = new CustomUserDetails(username, user.password, user.enabled, false, false, false, authorities, user.id, user.firstName, user.lastName)

        return userDetails
        }

    @Override
    public void mapUserToContext(UserDetails arg0, DirContextAdapter arg1) {
    }
}

src/grovvy/CustomUserDetails.groovy

 package com.example


 import org.springframework.security.core.GrantedAuthority
 import org.springframework.security.core.userdetails.User


 class CustomUserDetails extends User{
         final String firstName
         final String lastName

         CustomUserDetails(String username, String password, boolean enabled,
                           boolean accountNonExpired, boolean credentialsNonExpired,
                           boolean accountNonLocked,
                           Collection<GrantedAuthority> authorities,
                           long id, String firstName, String lastName) {
             super(username, password, enabled, accountNonExpired,
                     credentialsNonExpired, accountNonLocked, authorities, id)

             this.firstName = firstName
             this.lastName = lastName
         }
     }

src/groovy/CustomUserDetailsService.groovy

package com.example


import grails.plugin.springsecurity.userdetails.GrailsUserDetailsService
import  org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UsernameNotFoundException


class CustomUserDetailsService implements GrailsUserDetailsService {

   /**
    * Some Spring Security classes (e.g. RoleHierarchyVoter) expect at least one role, so
    * we give a user with no granted roles this one which gets past that restriction but
    * doesn't grant anything.
    */
   static final List NO_ROLES = [new SimpleGrantedAuthority("NO_ROLE")]

   UserDetails loadUserByUsername(String username, boolean loadRoles)
   throws UsernameNotFoundException {
       return loadUserByUsername(username)
   }

   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

       User.withTransaction { status ->

           User user = User.findByUsername(username)
           if (!user) throw new UsernameNotFoundException('User not found', username)

           def authorities = user.authorities.collect {new SimpleGrantedAuthority(it.authority)}

           return new CustomUserDetails(user.username, user.password, user.enabled,
                   !user.accountExpired, !user.passwordExpired,
                   !user.accountLocked, authorities ?: NO_ROLES, user.id,
                   user.firstName, user.lastName)
       } as UserDetails
   }
 }

conf/resources.groovy

// Place your Spring DSL code here
import com.example.CustomUserDetailsContextMapper
import com.example.CustomUserDetailsService

beans = {
  userDetailsService(CustomUserDetailsService)

    ldapUserDetailsMapper(CustomUserDetailsContextMapper) {
        dataSource = ref("dataSource")
    }
} 

When I run with this configuration and try to login I get the following error message:

 Message: object references an unsaved transient instance - save the transient instance before flushing: com.example.User; nested exception is org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.example.User
Was it helpful?

Solution

I had the same problem. The error message states that the User instance wasn't saved. I fixed it by changing the following line in the CustomUserDetailsMapper.grovvy

user = new User(username: username, enabled: true, firstName: firstName, lastName: lastName)

to

user = new User(username: username, enabled: true, firstName: firstName, lastName: lastName, accountLocked: false, passwordExpired: false, accountExpired: false, password: "test")

and by adding a firstName and lastName to the User domain class.

As you can see I just added some default values to user which is supposed to be created. It doesn't matter that the password is always set to "test". It won't be used because you are using LDAP.

OTHER TIPS

For anyone that instead modified the generated user class from spring security(after running the quickstart script) and got the same error, I have added the nullable: true in the static constraints in the domain user class to all of your custom properties - in this case firstName and lastName. This allows you to save the instance without setting all of the properties explicitly.

Hope this helps someone!

static constraints = { username blank: false, unique: true password blank: false fname nullable: true lname nullable: true }

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top