Let’s say we have web application running in intranet on some simple server container which is running on Linux machine (I’m using CentOS 7.4 in this example) and we want to have our users logged in if they are already logged in their Windows machines. Better to picture this:

Network and hardware architecture used in this post

There is a Spring documentation about this topic (https://docs.spring.io) but I was forced to take few steps during configuration which are not described and I encountered few problems which I want to share with you. At the beginning, I assume that computers must obviously be visible to each other in the network! When creating an example configuration I used JDK 8 and the following dependencies:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-ldap</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.kerberos</groupId>
            <artifactId>spring-security-kerberos-web</artifactId>
            <version>1.0.1.RELEASE</version>
        </dependency>
		<!--Lombok dependency it's used only to allow logging annotations in this example-->
       <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

Step one - Windows Domain Server

The steps that must be taken at this point must be performed when logged on to a Windows server running Active Directory service containing user data of our application. First I created user account in Active Directory which will be used by application to authenticate. To do this you need click Start, point to Programs, point to Administrative Tools, and then click Active Directory Users and Computers (you can follow instructions from MSDN). In my case example username is tomcat. I set a password to newly created user and I unchecked option which force user to change his password on first logon. After creating the user, open terminal and execute the following commands:

setspn -A HTTP/applicationhost@YOURDOMAIN.COM tomcat

…where applicationhost@YOURDOMAIN.COM is the address (host name) and domain where our application resides and tomcat is user that you created in previous step. This is an important point and it is important that this is consistent with the address of our application and subsequent configuration steps. NOTE: You can not use IP, it must be a host/domain name of application server computer. In case of when our web application does not have a domain name yet (in DNS), we should at least assign host name to server IP on any machine which is connecting to it by editing hosts file. On Windows it’s C:\Windows\System32\drivers\etc\hosts file. On Linux system it’s /etc/hosts file. Another command is used to create the necessary file which contains Kerberos keys:

ktpass /out c:\tomcat.keytab /mapuser tomcat@YOURDOMAIN.COM /princ HTTP/applicationhost@YOURDOMAIN.COM /pass TomcatUserFunnyPassword /ptype KRB5_NT_PRINCIPAL /crypto All

where tomcat@YOURDOMAIN.COM is the user that we created at the beginning, YOURDOMAIN.COM is the domain in which this user was created in, /princ HTTP/applicationhost@YOURDOMAIN.COM must match what we provide in the first command. The password (string after /pass) must match the tomcat user’s password that we set up when we create it in the first place. If command end up successfull it will generate a file (c:\tomcat.keytab) that must be placed in a folder already available from the application (from the application server), I will explain that in next section.

Step two - preparing the application server side (Linux system)

In case the application server is on Windows it is sufficient to just copy tomcat.keytab file created in accordance with the instructions from the first step to path available by the server (eg.: c:\) and configure the path in the application configuration (as described in Step Three), and that should suffice. However, if your application is on Linux sometimes you need to tweak your system configuration. This was the case with my CentOS 7.4 system. Anyway you should first try to copy the tomcat.keytab file to the /opt/tomcat.keytab directory (or whatever else you set up later in the application configuration) and configure application security config (as described in step three) and try it without taking extra steps described below and only If that fails please follow them.

As I wrote, if the steps above fail you should follow steps below. Some steps labeled ,,Required for manual connection’’ are not required, however, they could help detect a potential error:

  • Install a package that allow to log in to Active Directory using Kerberos: yum install cyrus-sasl-gssapi ,
  • If the AD server does not have a DNS name in the network then edit /etc/hosts file and add the line with host name corresponding to the IP of the Active Directory server: 192.168.1.201 activedirectoryserverhostname (in my case server has IP 192.168.1.201) ,
  • (Required for manual connection) To run the connection tests from the terminal level, install the package: yum install krb5-workstation
  • (Required for manual connection) Replace the default Kerberos configuration file found in /etc/krb5.conf as follows:
[libdefaults]
default_realm = YOURDOMAIN.COM
default_keytab_name = /opt/tomcat.keytab
forwardable=true

[realms]
SWDP.PL = {
  kdc = applicationhost.YOURDOMAIN.COM:88
}

[domain_realm]
swdp.pl=YOURDOMAIN.COM
.swdp.pl=YOURDOMAIN.COM
  • (Required for manual connection) To test connection to AD server, you need to load the keytab file with the command: kinit -kt /opt/tomcat.keytab HTTP/applicationhost@YOURDOMAIN.COM, then you can check with klist command that key is properly loaded. Executing the klist command should write something like this:
Default principal: HTTP/applicationhost@YOURDOMAIN.COM

Valid starting       Expires              Service principal
10/23/2017 15:48:31  10/24/2017 01:48:31  krbtgt/YOURDOMAIN.COM@YOURDOMAIN.COM
        renew until 10/24/2017 15:48:31
  • (Required for manual connection) Once the keys have been loaded, you can perform a trial connection, for example:
ldapwhoami -Y GSS-SPNEGO -v -h activedirectoryserverhostname -v

# OR

ldapsearch -Y GSS-SPNEGO -H ldap://activedirectoryserverhostname -b "dc = YOURDOMAIN, dc = com "

Commands should end successfully. The address activedirectoryserverhostname is the host name of the Active Directory server obtained from hosts file or DNS server.

Step three - Spring Security configruation

Below is the configuration of Spring Security. Pay attention to the comments because there are the most important information:

@Slf4j //Lombok annotation for logging
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .exceptionHandling()
                .authenticationEntryPoint(spnegoEntryPoint())
                .and()
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login").permitAll()
                .and()
            .logout()
                .permitAll()
                .and()
            .addFilterBefore(
                    spnegoAuthenticationProcessingFilter(authenticationManagerBean()),
                    BasicAuthenticationFilter.class);
    }

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

    @Bean
    public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
        KerberosAuthenticationProvider provider =
                new KerberosAuthenticationProvider();
        SunJaasKerberosClient client = new SunJaasKerberosClient();
        client.setDebug(true);
        provider.setKerberosClient(client);
        provider.setUserDetailsService(dummyUserDetailsService());
        return provider;
    }

    @Bean
    public SpnegoEntryPoint spnegoEntryPoint() {
        return new SpnegoEntryPoint("/login");
    }

    @Bean
    public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter() {
        SpnegoAuthenticationProcessingFilter filter =
                new SpnegoAuthenticationProcessingFilter();
        try {
            filter.setAuthenticationManager(authenticationManagerBean());
        } catch (Exception e) {
            log.error("Failed to set AuthenticationManager on SpnegoAuthenticationProcessingFilter.", e);
        }
        return filter;
    }

    @Bean
    public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() {
        KerberosServiceAuthenticationProvider provider =
                new KerberosServiceAuthenticationProvider();
        provider.setTicketValidator(sunJaasKerberosTicketValidator());
        provider.setUserDetailsService(dummyUserDetailsService());
        return provider;
    }

   @Bean
    public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
        SunJaasKerberosTicketValidator ticketValidator =
                new SunJaasKerberosTicketValidator();
        ticketValidator.setServicePrincipal("HTTP/applicationhost@YOURDOMAIN.COM"); //At this point, it must be according to what we were given in the commands from the first step.
        FileSystemResource fs = new FileSystemResource("/opt/tomcat.keytab"); //Path to file tomcat.keytab
        log.info("Initializing Kerberos KEYTAB file path:" + keytabFilePath);
        Assert.notNull(fs.exists(), "*.keytab key must exist. Without that security is useless.");
        ticketValidator.setKeyTabLocation(fs);
        ticketValidator.setDebug(true); //Turn off when it will works properly,
        return ticketValidator;
    }

    @Bean
    public DummyUserDetailsService dummyUserDetailsService() {
        return new DummyUserDetailsService();
    }
}

Below is a simple class needed to load detailed user data. The class is mock but you can use it and then extend. In real life example we would be probably reading information about user groups from Active Directory or database.

@Slf4j //Lombok annotation for logging
public class DummyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        log.info(username);
        return new User(username, "notUsed", true, true, true, true,
                AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN"));
    }
}

Troubleshooting

The protocol is quite irritating on some issues, and it is very important to go through these steps carefully to properly prepare the authentication. Here are some errors you might encounter:

  1. Please note that the tomcat.keytab file was loaded during the initialization of the security configuration because its failure causes errors where the messages do not tell us much.
  2. Excessive time difference (greater than 5 minutes) between the server on which the application is running and the AD server may cause an error:
     Failure unspecified at GSS-API level (Mechanism level: Clock skew too great (37)) .
    

    The solution may be to synchronize the machine time on which the application server is running with the AD server (for example using NTP).

  3. Please note that the host / domain address on which the application is running is consistent with the configuration and whether or not it is actually used when accessing the application (and not IP for example).
  4. Be sure to install the cyrus-sasl-gsasapi package because when it’s missing Kerberos communicate lack of authentication managers.

Summary

That’s it! You should be now automatically logged in to your application using Active Directory. Thanks for reading!