Blog article

Windows Authentication in Java

by

If you want to restrict the users of an application, the first solution is to make them authenticate themselves. The user enters his or her user name and password and after successful verification of the values the program can be used.

Since storing login credentials is a problem, I present a simple solution in this article: delegating the authentication function to a Windows domain controller. The user logs on with his or her Windows user name and password, which is checked by the Windows server.

Which protocol do I use for authentication?

„When in Rome, do as the Romans do.‟

I use the protocol that Windows itself uses for its domain logon: Kerberos. Kerberos is a challenge-response method designed for use in insecure networks. No passwords are transmitted in the network, and the retrieval of login packages for replay attacks is also ineffective (more about Kerberos here).

I use JAAS (Java Authentication and Authorization Service) as the Java API for my implementation. My implementation is configuration-free, except for the name of the Windows domain, no other data is required. Therefore, there is no JAAS configuration file that needs to be customized or deployed.

You could also perform the logon using an LDAP bind. This is not recommended, however, as the LDAP protocol likes to transfer passwords in clear text and an SSL connection (ldaps:) is therefore mandatory. In addition, there can be considerable configuration effort if the LDAP standard ports are not used or SSL certificates are needed for LDAP client authentication (depending on what local network administration requires).

How do I find the Windows server to authenticate against?

The available Windows servers that can perform logon functions are registered in the DNS. We determine the servers via a DNS query.

/**
  * Get Active Directory domain controllers.
  *
  * Shell example: nslookup -type=SRV _ldap._tcp.dc._msdcs.mydomain.local
  *
  * @param domain
  *            Domain name (e.g. "mydomain.local")
  * @return Domain controllers (list may be empty)
  * @throws NamingException
  */
 private static Collection<InetSocketAddress> getDomainControllers(String domain) throws NamingException {

     final String typeSRV = "SRV";
     final String[] types = new String[] { typeSRV };

     DirContext ctx = new InitialDirContext();

     Attributes attributes = ctx.getAttributes("dns:/_ldap._tcp.dc._msdcs." + domain, types);
     if (attributes.get(typeSRV) == null) {
         return Collections.emptyList();
     }

     NamingEnumeration<?> e = attributes.get(typeSRV).getAll();
     TreeMap<Integer, InetSocketAddress> result = new TreeMap<>();

     while (e.hasMoreElements()) {

         String line = (String) e.nextElement();

         // The line is: priority weight port host
         String[] parts = line.split("\\s+");

         int prio = Integer.parseInt(parts[0]);
         int port = Integer.parseInt(parts[2]);
         String host = parts[3];

         result.put(prio, new InetSocketAddress(host, port));
     }

     return result.values();
 }

In large domains, the function may return 20 or more servers. Since a DNS load balancing usually takes place anyway and our function is only rarely called, I do not evaluate the server priorities in the further program and simply use the first delivered server.

Configure JAAS

The JAAS API requires a configuration. We do not, so we simply implement an empty configuration class.

/**
  * JAAS configuration.
  */
 public static class StaticConfiguration extends Configuration {

     final AppConfigurationEntry staticConfigEntry;

     public StaticConfiguration(String loginModuleName) {

         Map<String, ?> options = new HashMap<>();
         staticConfigEntry = new AppConfigurationEntry(loginModuleName,
                 AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
     }

     @Override
     public AppConfigurationEntry[] getAppConfigurationEntry(String name) {

         return new AppConfigurationEntry[] { staticConfigEntry };
     }
 }

The JAAS configuration requires a handler that is usually used to interactively query logon data from the user. In our case, however, the data comes via API, so that we implement a handler that transfers these values to the JAAS.

/**
  * JAAS callback handler.
  */
 public static class StaticCallbackHandler implements CallbackHandler {

     /**
      * Constructor.
      *
      * @param username
      *            Windows user name
      * @param password
      *            Windows password
      */
     public StaticCallbackHandler(String username, String password) {

         this.username = username;
         this.password = password;
     }

     @Override
     public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {

         for (int i = 0; i < callbacks.length; i++) {

             if (callbacks[i] instanceof TextOutputCallback) {

                 // unused

             } else if (callbacks[i] instanceof NameCallback) {

                 NameCallback nc = NameCallback.class.cast(callbacks[i]);
                 nc.setName(username);

             } else if (callbacks[i] instanceof PasswordCallback) {

                 PasswordCallback pc = PasswordCallback.class.cast(callbacks[i]);
                 pc.setPassword(password.toCharArray());

             } else {

                 throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
             }
         }
     }

     /** User name. */
     private String username;

     /** Password. */
     private String password;
 }

Perform the Windows logon

The following code example contains the actual logon function. First a suitable Windows server is searched for, then the JAAS subsystem is configured for Kerberos. The login context object is then used to perform the actual logon.

/**
  * Constructor.
  *
  * @param domainName
  *            domain name (e.g. "mydomain.local")
  */
 public ActiveDirectoryAuthentication(String domainName) {

     this.domainName = domainName;
 }

 /**
  * Authenticate user.
  *
  * @param username
  *            Windows user name
  * @param password
  *            Windows password
  * @throws ValidationException
  */
 public void authenticate(String username, String password) throws ValidationException {

     LoginContext lc;
     try {

         // get domain controller for login
         Collection<InetSocketAddress> result = getDomainControllers(domainName);
         if (result.isEmpty()) {
             throw new ValidationException("No domain controllers found for domain " + domainName);
         }
         String loginServer = result.iterator().next().getHostString();
         System.setProperty("java.security.krb5.realm", domainName.toUpperCase());
         System.setProperty("java.security.krb5.kdc", loginServer);

         // perform login
         lc = new LoginContext("", null, new StaticCallbackHandler(username, password),
                 new StaticConfiguration("com.sun.security.auth.module.Krb5LoginModule"));
         lc.login();

         // logout (we want to check the password only)
         lc.logout();

     } catch (LoginException le) {

         // error
         throw new ValidationException("Authentication failed: " + le.getMessage(), le);

     } catch (SecurityException se) {

         // error
         throw new ValidationException("Authentication failed: " + se.getMessage(), se);

     } catch (NamingException ne) {

         // error
         throw new ValidationException("Authentication failed: " + ne.getMessage(), ne);
     }
 }

 /** Windows domain name. */
 private String domainName;
}

Conclusion

The example shows that Windows authentication is easy to implement in Java. Since there are no complex configurations and dependencies, the code example can be used in very different environments.

Go back