How to implement (external LDAP) authentication in a Zimbra Java Extension

Frequent readers of the Zimbra blog will know that Zimbra can be extended/customized by using Zimlets. By creating your own Zimlets you can add functionality to the UI (front-end) and the Java back-end, allowing you to cater to specific customer needs. Zimlets can be enabled globally or per user (group)

Details on this can be found in our wiki: https://wiki.zimbra.com/wiki/DevelopersGuide and https://blog.zimbra.com/2020/05/zimbra-skillz-extending-zimbras-backend-functionality-with-zimlets/

In this blog you will learn:

  1. How to implement Zimbra authentication so that only a logged in Zimbra user can access your extension.
  2. How to implement secure external LDAP authentication in a Zimbra extension.

On the Internet many examples can be found to do this, but most examples are incomplete and/or insecure, so even if you already implemented Zimbra or external LDAP authentication in Java before, please read the rest of the blog and validate your implementation.

Implementing Zimbra authentication in a Zimbra extension

If we take a look at the example extension at: https://github.com/Zimbra/zm-extension-guide/blob/master/src/com/example/mytest/Mytest.java#L55 you will see that there is no authentication needed in the doGet method. doGet is what we implement to handle HTTP GET requests in Zimbra extensions.

You can use the following Zimbra Java API as a wrapper to implement Zimbra authentication:

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
   final AuthToken authToken = AuthUtil.getAuthTokenFromHttpReq(req, resp, false, true);
   if (authToken != null) {
      //This code is only accessible for authenticated users
   }
   else
   {
      //Do something for not auhtenticated users
   }
}

If you also want to implement a CSRF check you can take a look at: https://github.com/Zimbra/zm-extension-guide/blob/master/Implementing%20a%20CSRF%20check%20on%20a%20Zimbra%20extension.md

There are many other examples in (old) Zimbra code on the Internet, most of the time these will have some iterator that finds the Zimbra authentication cookie and then validate the cookie and get a Class of Service or other account parameter. Many of these examples lack one or more authentication validations such as password expiration, account locking etc. The use of AuthUtil.getAuthTokenFromHttpReq is recommended as this is the API that is designed for and implements the full Zimbra authentication check.

Implementing external LDAP authentication in Java

For external LDAP authentication we use exiting Java API’s, meaning not Zimbra specific ones, you can implement a username/password check to an external LDAP server as follows:

   public static Boolean authenticate(String username, String password) {
      try 
      {
         if (StringUtils.isEmpty(password) || StringUtils.isBlank(password)) {
            ZimbraLog.account.info("Password Authentication failure");
            return false;
         }
         if (password.length() < 10) {
            ZimbraLog.account.info("password too short, minimum length: 10");
            return false;
         }

         // Set up environment for creating initial context
         Hashtable<String, String> env = new Hashtable<>();
         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
         env.put(Context.PROVIDER_URL, "ldap://myldap.example.com:389");
         LdapContext ctx = new InitialLdapContext(env, null);
         StartTlsResponse tls;
         try {
            tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
            tls.negotiate();
         } catch (Exception e) {
            ZimbraLog.account.info("My Zimbra extension reports: LDAP TLS failure");
            return false;
         }

         // Perform simple client authentication
         ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
         ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, DNify(username));
         ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
         ZimbraLog.account.debug(ctx.lookup(DNify(username))); //This will throw an exception when authentication fails!
         tls.close();
         ctx.close();

         //If we are here the username/password passed is correct, and we can proceed to get the Zimbra account object
         Account account = Provisioning.getInstance().getAccountByName(username);
         //do something here
         return true;
      } catch (Exception e) {
         ZimbraLog.account.info(" LDAP Password Authentication failure");
         return false;
      }
      return false;
   }

   /* Convert email address to DN, is there a better way?
   System.out.println(DNify("info.urft@mail.barrydegraaff.nl"));
   prints:
   uid=info.urft,ou=people,dc=mail,dc=barrydegraaff,dc=nl
   * */
   public static String DNify(String email) {
      String[] split = email.split("@");
      return "uid=" + split[0] + ",ou=people,dc=" + split[1].replaceAll("\\.", ",dc=");
   }

See also: https://docs.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html

The above LDAP authentication example will force the use of STARTTLS on the connection between Zimbra and your LDAP server. Many examples on the Internet will use something like:

env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://myldap.example.com:389");
...
env.put(Context.SECURITY_PROTOCOL, "ssl");
DirContext ctx = new InitialDirContext(env);

This will NOT force the use of STARTTLS or TLS.

By using StartTlsResponse the Java LDAP client (com.sun.jndi.ldap.LdapCtxFactory) can not fall back to plain text communication. Meaning TLS will be used between your Java application and the LDAP server. If STARTTLS fails, an exception will be thrown and authentication fails regardless if the user entered a correct username/password.

, ,

No comments yet.

Leave a Reply

Copyright © 2022 Zimbra, Inc. All rights reserved.

All information contained in this blog is intended for informational purposes only. Synacor, Inc. is not responsible or liable in any manner for the use or misuse of any technical content provided herein. No specific or implied warranty is provided in association with the information or application of the information provided herein, including, but not limited to, use, misuse or distribution of such information by any user. The user assumes any and all risk pertaining to the use or distribution in any form of any subject matter contained in this blog.

Legal Information | Privacy Policy | Do Not Sell My Personal Information | CCPA Disclosures