22

I implemented database authentication for my web page and web service. It work well for both, now I have to add Ldap authentication. I have to authenticate through remote Ldap server (using username and password) and if the user exists I have to use my database for user roles (in my database username is the same username of Ldap). So I have to switch from my actual code to the Ldap and database authentication as above explained. My code is: SecurityConfig class

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, proxyTargetClass = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("userDetailsService")
    UserDetailsService userDetailsService;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        return encoder;
    }

    @Configuration
    @Order(1)
    public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity http) throws Exception {
             http.csrf().disable()
             .antMatcher("/client/**")
             .authorizeRequests()
             .anyRequest().authenticated()
             .and()
             .httpBasic();
        }
    }

    @Configuration
    @Order(2)
    public static class FormWebSecurityConfig extends WebSecurityConfigurerAdapter{

        @Override
        public void configure(WebSecurity web) throws Exception {
            web
            //Spring Security ignores request to static resources such as CSS or JS files.
            .ignoring()
            .antMatchers("/static/**");
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
            .authorizeRequests() //Authorize Request Configuration
                //the / and /register path are accepted without login
                //.antMatchers("/", "/register").permitAll()
                //the /acquisition/** need admin role
                //.antMatchers("/acquisition/**").hasRole("ADMIN")
                //.and().exceptionHandling().accessDeniedPage("/Access_Denied");
                //all the path need authentication
                .anyRequest().authenticated()
                .and() //Login Form configuration for all others
            .formLogin()
                .loginPage("/login")
                //important because otherwise it goes in a loop because login page require authentication and authentication require login page
                    .permitAll()
            .and()
            .logout()
                .logoutSuccessUrl("/login?logout")
                .permitAll();
             // CSRF tokens handling
        }
    }

MyUserDetailsService class

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserServices userServices;
    static final Logger LOG = LoggerFactory.getLogger(MyUserDetailsService.class);

    @Transactional(readOnly=true)
    @Override
    public UserDetails loadUserByUsername(final String username){
        try{
            com.domain.User user = userServices.findById(username);
            if (user==null)
                LOG.error("Threw exception in MyUserDetailsService::loadUserByUsername : User doesn't exist" ); 
            else{
                List<GrantedAuthority> authorities = buildUserAuthority(user.getUserRole());
                return buildUserForAuthentication(user, authorities);
            }
        }catch(Exception e){
            LOG.error("Threw exception in MyUserDetailsService::loadUserByUsername : " + ErrorExceptionBuilder.buildErrorResponse(e));  }
        return null;
    }

    // Converts com.users.model.User user to
    // org.springframework.security.core.userdetails.User
    private User buildUserForAuthentication(com.domain.User user, List<GrantedAuthority> authorities) {
        return new User(user.getUsername(), user.getPassword(), user.isEnabled(), true, true, true, authorities);
    }

    private List<GrantedAuthority> buildUserAuthority(Set<UserRole> userRoles) {

        Set<GrantedAuthority> setAuths = new HashSet<GrantedAuthority>();

        // Build user's authorities
        for (UserRole userRole : userRoles) {
            setAuths.add(new SimpleGrantedAuthority(userRole.getUserRoleKeys().getRole()));
        }

        List<GrantedAuthority> Result = new ArrayList<GrantedAuthority>(setAuths);

        return Result;
    }

so I have to:

1)access of user from login page for web pages and username and password for web services. This has to be done through Ldap.

2)the username of user needs for database query to authenticate user. Do you have any idea how I can implement this? Thanks

UPDATE WITH RIGHT CODE: Following the @M. Deinum advice I create MyAuthoritiesPopulator class instead of MyUserDetailsService and authentication with database and Ldap works:

    @Service("myAuthPopulator")
public class MyAuthoritiesPopulator implements LdapAuthoritiesPopulator {

    @Autowired
    private UserServices userServices;
    static final Logger LOG = LoggerFactory.getLogger(MyAuthoritiesPopulator.class);

    @Transactional(readOnly=true)
    @Override
    public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        try{
            com.domain.User user = userServices.findById(username);
            if (user==null)
                LOG.error("Threw exception in MyAuthoritiesPopulator::getGrantedAuthorities : User doesn't exist into ATS database" );  
            else{
                for(UserRole userRole : user.getUserRole()) {
                    authorities.add(new SimpleGrantedAuthority(userRole.getUserRoleKeys().getRole()));
                }
                return authorities;
            }
        }catch(Exception e){
            LOG.error("Threw exception in MyAuthoritiesPopulator::getGrantedAuthorities : " + ErrorExceptionBuilder.buildErrorResponse(e)); }
        return authorities;
    }
}

and I changed SecurityConfig as below:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, proxyTargetClass = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("myAuthPopulator")
    LdapAuthoritiesPopulator myAuthPopulator;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

         auth.ldapAuthentication()
          .contextSource()
            .url("ldap://127.0.0.1:10389/dc=example,dc=com")
//          .managerDn("")
//          .managerPassword("")
          .and()   
            .userSearchBase("ou=people")
            .userSearchFilter("(uid={0})")
            .ldapAuthoritiesPopulator(myAuthPopulator);     
    }

    @Configuration
    @Order(1)
    public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity http) throws Exception {
             http.csrf().disable()
             .antMatcher("/client/**")
             .authorizeRequests()
             //Excluede send file from authentication because it doesn't work with spring authentication
             //TODO add java authentication to send method
             .antMatchers(HttpMethod.POST, "/client/file").permitAll()
             .anyRequest().authenticated()
             .and()
             .httpBasic();
        }
    }

    @Configuration
    @Order(2)
    public static class FormWebSecurityConfig extends WebSecurityConfigurerAdapter{

        @Override
        public void configure(WebSecurity web) throws Exception {
            web
            //Spring Security ignores request to static resources such as CSS or JS files.
            .ignoring()
            .antMatchers("/static/**");
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
            .authorizeRequests() //Authorize Request Configuration
                //the "/" and "/register" path are accepted without login
                //.antMatchers("/", "/register").permitAll()
                //the /acquisition/** need admin role
                //.antMatchers("/acquisition/**").hasRole("ADMIN")
                //.and().exceptionHandling().accessDeniedPage("/Access_Denied");
                //all the path need authentication
                .anyRequest().authenticated()
                .and() //Login Form configuration for all others
            .formLogin()
                .loginPage("/login")
                //important because otherwise it goes in a loop because login page require authentication and authentication require login page
                    .permitAll()
            .and()
            .logout()
                .logoutSuccessUrl("/login?logout")
                .permitAll();
        }
    }
}

My LDAP development environment created in Apache directory studio

ldap

luca
  • 3,248
  • 10
  • 66
  • 145
  • Have you read the reference guide that has a [whole chapter on ldap](http://docs.spring.io/spring-security/site/docs/4.0.3.RELEASE/reference/htmlsingle/#ldap) and which components are there. – M. Deinum Jan 07 '16 at 15:31
  • I read it quickly because I use annotation and not xml. I'm reading it again now – luca Jan 07 '16 at 15:43
  • So the answer you can also configure it using Java. – M. Deinum Jan 07 '16 at 15:44
  • authentication through database? there are a lot of example online – luca May 24 '18 at 06:06

4 Answers4

11

Spring Security already supports LDAP out-of-the-box. It actually has a whole chapter on this.

To use and configure LDAP add the spring-security-ldap dependency and next use the AuthenticationManagerBuilder.ldapAuthentication to configure it. The LdapAuthenticationProviderConfigurer allows you to set the needed things up.

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.ldapAuthentication()
      .contextSource()
        .url(...)
        .port(...)
        .managerDn(...)
        .managerPassword(...)
      .and()
        .passwordEncoder(passwordEncoder())
        .userSearchBase(...)        
        .ldapAuthoritiesPopulator(new UserServiceLdapAuthoritiesPopulater(this.userService));      
}

Something like that (it should give you at least an idea on what/how to configure things) there are more options but check the javadocs for that. If you cannot use the UserService as is to retrieve the roles (because only the roles are in the database) then implement your own LdapAuthoritiesPopulator for that.

M. Deinum
  • 115,695
  • 22
  • 220
  • 224
  • 2
    I know that Spring security supports LDAP but I have to make an hybrid between LDAP and database and I don't find a valid example also for LDAP only. – luca Jan 07 '16 at 16:36
  • Have you actually read my answer and have read the javadocs? Judging from the comment you haven't. – M. Deinum Jan 07 '16 at 18:51
  • Yes, it explains that "If you want to use LDAP only for authentication, but load the authorities from a difference source (such as a database) then you can provide your own implementation of this interface and inject that instead." So I have updated my first post – luca Jan 08 '16 at 09:24
  • Have you tested your modified solution? Why wouldn't it be the right way (you still have to fix your ldap configuration based on your structure in there). – M. Deinum Jan 08 '16 at 09:31
  • I receive the above exception. Yes, then I have to fix even my LDAP configuration – luca Jan 08 '16 at 10:26
  • Have you actually read the message? You haven't provided anything to search for so your config is wrong... We cannot help you with that as we don't know how your LDAP is structured... – M. Deinum Jan 08 '16 at 10:31
  • if fix it, but receive connection refused as above,if I test my Ldap with JXplorer it works? – luca Jan 08 '16 at 13:34
  • then your url or username/pwd isn't correct... Else you wouldn't get that exception... – M. Deinum Jan 08 '16 at 13:39
  • Perfect, it works, thanks. I have only one question: with the current code if the user doesn't exist into database it is logged without roles but I have to deny the access like it wouldn't exist into Ldap. For the moment I use hasAnyRole("Admin", "User"...) writing all the roles – luca Jan 08 '16 at 16:28
  • You could try throwing a `UsernameNotFoundException` from your custom `LdapAuthoritiesPopulator`. – M. Deinum Jan 10 '16 at 09:36
  • If you're talking about only using LDAP, for both authentication and authorization, you'll want to assign users to Organizational Unit groups within AD. You can then apply your ldap base so that it only searches within the specified OU. – Jolley71717 Feb 26 '18 at 16:50
  • 1
    @M.Deinum all your links are dead which you have shared with us.Please check them. – Ömür Alçin Aug 31 '20 at 05:58
2

You need to create a CustomAuthenticationProvider wich implements AuthenticationProvider, and override authenticate method, for example:

@Component
public class CustomAuthenticationProvider
    implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        boolean authenticated = false;
        /**
         * Here implements the LDAP authentication
         * and return authenticated for example
         */
        if (authenticated) {

            String usernameInDB = "";
            /**
             * Here look for username in your database!
             * 
             */
            List<GrantedAuthority> grantedAuths = new ArrayList<>();
            grantedAuths.add(new     SimpleGrantedAuthority("ROLE_USER"));
            Authentication auth = new     UsernamePasswordAuthenticationToken(usernameInDB, password,     grantedAuths);
            return auth;
        } else {
            return null;
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return     authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

Then, in your SecurityConfig, you need to override the configure thats use AuthenticationManagerBuilder:

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(this.authenticationProvider);
}

You can autowire the CustomAuthenticationProvider doing this:

@Autowired
private CustomAuthenticationProvider authenticationProvider;

Doing this, you can override the default authentication behaviour.

melli-182
  • 1,216
  • 3
  • 16
  • 29
  • thanks for the reply. For LDAP authentication you mean spring authentication or only check if user and password exists? – luca Jan 07 '16 at 15:28
  • Why? Why so complex. Spring Security out-of-the-box supports Ldap, configure it right. Then instead of retrieving the roles from ldap use a database drivwn `LdapAuthoritiesPopulator` (there is already a user service driven one so it all might be a simple matter of configuration). – M. Deinum Jan 07 '16 at 15:30
  • @luca with this implementation, you can override the default behaviour of AuthenticationProvider. I did something similar beacause i have a thirdParty app whichs provides me the authentication service, but with Ldap must be pretty similar. – melli-182 Jan 07 '16 at 15:38
1

I also found this chapter Spring Docu Custom Authenicator and build my own switch between LDAP and my DB users. I can effortlessy switch between login data with set priorities (in my case LDAP wins).

I have configured an LDAP with the yaml configuration files for the LDAP user data which I don't disclose here in detail. This can be easily done with this Spring Docu LDAP Configuration.

I stripped the following example off the clatter such as logger/javadoc etc. to highlight the important parts. The @Order annotation determines the priorities in which the login data is used. The in memory details are hardcoded debug users for dev only purposes.

SecurityWebConfiguration

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Inject
  private Environment env;
  @Inject
  private LdapConfiguration ldapConfiguration;

  @Inject
  private BaseLdapPathContextSource contextSource;
  @Inject
  private UserDetailsContextMapper userDetailsContextMapper;

  @Inject
  private DBAuthenticationProvider dbLogin;

  @Inject
  @Order(10) // the lowest number wins and is used first
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(new InMemoryUserDetailsManager(getInMemoryUserDetails()));
  }

  @Inject
  @Order(11) // the lowest number wins and is used first
  public void configureLDAP(AuthenticationManagerBuilder auth) throws Exception {
    if (ldapConfiguration.isLdapEnabled()) {
      auth.ldapAuthentication().userSearchBase(ldapConfiguration.getUserSearchBase())
          .userSearchFilter(ldapConfiguration.getUserSearchFilter())
          .groupSearchBase(ldapConfiguration.getGroupSearchBase()).contextSource(contextSource)
          .userDetailsContextMapper(userDetailsContextMapper);
    }
  }

  @Inject
  @Order(12) // the lowest number wins and is used first
  public void configureDB(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(dbLogin);
  }
}

DB Authenticator

@Component
public class DBAuthenticationProvider implements AuthenticationProvider {

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    String name = authentication.getName();
    String password = authentication.getCredentials().toString();

   // your code to compare to your DB
  }

  @Override
  public boolean supports(Class<?> authentication) {
    return authentication.equals(UsernamePasswordAuthenticationToken.class);
  }

  /**
   * @param original <i>mandatory</i> - input to be hashed with SHA256 and HEX encoding
   * @return the hashed input
   */
  private String sha256(String original) {
    MessageDigest md = null;
    try {
      md = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException e) {
      throw new AuthException("The processing of your password failed. Contact support.");
    }

    if (false == Strings.isNullOrEmpty(original)) {
      md.update(original.getBytes());
    }

    byte[] digest = md.digest();
    return new String(Hex.encodeHexString(digest));
  }

  private class AuthException extends AuthenticationException {
    public AuthException(final String msg) {
      super(msg);
    }
  }
}

Feel free to ask details. I hope this is useful for someone else :D

Dr4gon
  • 421
  • 8
  • 17
0

For anyone using grails it is much simpler. Simply add this to your config:

grails: plugin: springsecurity: ldap: authorities: retrieveDatabaseRoles: true

rhinmass
  • 174
  • 1
  • 7