Implementing Custom Authentication using a Zimbra extension (updated)

In this article you will learn how to implement Custom Authentication using a Zimbra extension. The Java project and source code can be found at https://github.com/Zimbra/zimbra-custom-authentication.

Take a look at https://github.com/Zimbra/zm-extension-guide if you are new to Java or building Zimbra extensions. The zm-extension-guide covers all the things needed to build the Custom Authentication extension.

Zimbra by default supports authenticating to LDAP, Active Directory, SAML and Pre-Auth (see further reading section below). In some cases you may want to implement a Custom Authentication extension for Zimbra. For example if you want to authenticate using an external REST API or if you want to implement additional restrictions for log-in such as restrictions per user, protocol (imap, soap, etc) and IP address.

Custom Authentication in Java

To implement the Custom Authentication extension you need to structure it as follows:

src
└── com
    └── zimbra
        └── customauthguide
            ├── customAuthGuideAuthHandler.java
            └── customAuthGuideExtension.java
META-INF
└── MANIFEST.MF

The file customAuthGuideExtension.java implements a Zimbra Extension as explained in https://github.com/Zimbra/zm-extension-guide. The customAuthGuideAuthHandler is instantiated in the line: new customAuthGuideAuthHandler().register(ID);

public class customAuthGuideExtension implements ZimbraExtension {
    // This string is used to refer to this extension
    public static final String ID = "customAuthGuide";
    /**
     * Defines a name for the extension. It must be an identifier.
     *
     * @return extension name
     */
    public String getName() {
        return ID;
    }

    /**
     * Initializes the extension. Called when the extension is loaded.
     *
     * @throws com.zimbra.common.service.ServiceException
     */
    public void init() throws ServiceException {

        //Custom Authentication Handler
        new customAuthGuideAuthHandler().register(ID);
    }

    /**
     * Terminates the extension. Called when the server is shut down.
     */
    public void destroy() {

    }
}

The file customAuthGuideAuthHandler.java is where the actual custom authentication needs to be implemented. Specifically the authenticate method is called each time a user tries to log-in.

package com.zimbra.customauthguide;

import java.util.*;

import com.zimbra.common.service.ServiceException;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.account.Account;
import com.zimbra.cs.account.AccountServiceException;
import com.zimbra.cs.account.auth.ZimbraCustomAuth;
import com.zimbra.cs.listeners.AuthListener;

public class customAuthGuideAuthHandler extends ZimbraCustomAuth {
    public void register(String id) {
        ZimbraCustomAuth.register(id, this);
    }

    @Override
    public void authenticate(Account account, String password, Map<String, Object> context, List<String> args) throws ServiceException {
        if (!isAuthenticated(account, password, context)) {
            AccountServiceException.AuthFailedServiceException afse = AccountServiceException.AuthFailedServiceException.AUTH_FAILED("customAuthGuide Authentication failed");
            AuthListener.invokeOnException(afse);
            throw afse;
        }
    }

    /*
    * Implement log-in validation here
    * */
    protected boolean isAuthenticated(Account account, String password, Map<String, Object> context) {
        //THIS IS JUST AN EXAMPLE, NEVER HARDCODE USERNAMES, PASSWORDS AND IP'S THIS WAY!!
        if (("testuser@example.com".equals(account.getName())) && ("test123".equals(password))) {
            if ("imap".equals(context.get("proto").toString())) {
                ZimbraLog.account.warn("customAuthGuide Authentication failed, IMAP is not permitted for this user %s", account.getName());
                return false;
            }

            //to make this work make sure to read https://wiki.zimbra.com/wiki/Secopstips#Log_the_correct_origination_IP
            if ("54.83.74.191".equals(context.get("ocip"))) {
                ZimbraLog.account.warn("customAuthGuide Authentication failed, IP is not permitted for this user %s", account.getName());
                return false;
            }
            ZimbraLog.account.info("customAuthGuide Authentication success %s", account.getName());
            return true; //only return true if authentication has succeeded!
        }
        return false;
    }
}

In this example implementation everything is hard-coded for sake of simplicity. Instead of hard-coding you can make REST API calls using normal Java code or implement database look-ups.

If the authenticate method returns without an exception it means the authentication was successful. Or in other words you MUST make sure to always throw an exception for authentication failures!

A number of useful parameters are passed to the authenticate method that you can use to implement the authentication:

Parameter Description

account

the account object of the user that want to authenticate, account.getName() returns the primary email address of the user

anp

Username passed in to the interface, contains the username as typed by the user. Useful if you need to know that the user logged in using an alias since Account.getName() will give you the real account name

password

the plain text password typed by the user

context

see description below

The context parameter holds meta data from the request that you can use to refine your custom authentication:

Variable Description

ocip

IP address from the Originating IP header

ua

User Agent of the client

proto

Protocol being logged into (http_basic, http_dav, im, imap, pop3, soap, spnego, zsync) please note that SMTP is not in this list, see below.

soapport

when proto is soap, soapport will be 7073 for SMTP connections and 8080 or similar for WebUI authentication, see below (since Zimbra 9 patch 29)

For the originating IP to work, the Zimbra server needs to be configured correctly see https://wiki.zimbra.com/wiki/Secopstips#Log_the_correct_origination_IP for more information.

The extension needs to be build as a jar and then configured as below.

Differentiating SMTP and WebUI authentication

Since Zimbra uses SOAP internally to authenticate SMTP connections, you will need to use both proto and soapport from Authentication Context (context) to tell the difference between them. When the proto equals to soap and soapport equals to 7073 the authentication for an SMTP connection is being done. For WebUI authentication the protocol is also soap and defaults to port 8080, although this can be a different port.

Here is a code snippet for determining SMTP authentication.

if (context.get("soapport") != null) {
   if (!context.get("soapport").equals(account.getServer().getMtaAuthPort())) {
      //This is an SMTP authentication attempt
   }
}

In a real world scenario this can be used to for supporting passtokens on SMTP and 2FA on the WebUI.

Configuring custom authentication on Zimbra

Install the jar file on Zimbra as follows:

mkdir /opt/zimbra/lib/ext/customAuthGuide
cp /tmp/customAuthGuide.jar /opt/zimbra/lib/ext/customAuthGuide

Create a test domain and user and enable this custom authentication as follows:

sudo su zimbra -
zmprov cd example.com
zmprov ca testuser@example.com thispassworddoesnotwork
zmprov md example.com zimbraAuthMech custom:customAuthGuide
zmprov md example.com zimbraAuthFallbackToLocal FALSE
zmmailboxdctl restart

You are now ready to test your extension by logging into the Web-UI using user testuser@example.com and the password test123, confirm that the Zimbra fallback password thispassworddoesnotwork does not work.

You can run tail -f /opt/zimbra/log/mailbox.log while testing to see the logs. An example of successful authentication:

2022-05-12 08:48:43,178 INFO  [qtp1335505684-15://localhost:8080/service/soap/BatchRequest] [name=testuser@example.com;oip=192.168.1.114;ua=zclient/9.0.0_GA_4258;soapId=7ab4b771;] account - customAuthGuide Authentication success testuser@example.com
2022-05-12 08:48:43,180 INFO  [qtp1335505684-15://localhost:8080/service/soap/BatchRequest] [name=testuser@example.com;oip=192.168.1.114;ua=zclient/9.0.0_GA_4258;soapId=7ab4b771;] account - Authentication successful for user: testuser@example.com

And various examples of failed authentication:

2022-05-12 08:50:21,072 INFO  [qtp1335505684-125://localhost:8080/service/soap/BatchRequest] [name=testuser@example.com;oip=192.168.1.114;ua=zclient/9.0.0_GA_4258;soapId=7ab4b781;] account - Error occurred during authentication: authentication failed for [testuser@example.com (customAuthGuide Authentication failed)]. Reason:  (customAuthGuide Authentication failed).
2022-05-12 08:50:21,073 INFO  [qtp1335505684-125://localhost:8080/service/soap/BatchRequest] [name=testuser@example.com;oip=192.168.1.114;ua=zclient/9.0.0_GA_4258;soapId=7ab4b781;] SoapEngine - handler exception: authentication failed for [testuser@example.com (customAuthGuide Authentication failed)],  (customAuthGuide Authentication failed)

...

2022-05-12 08:54:50,081 INFO  [qtp1335505684-116://localhost:8080/service/soap/BatchRequest] [name=testuser@example.com;oip=192.168.1.114;ua=zclient/9.0.0_GA_4258;soapId=72aefed5;] account - Error occurred during authentication: authentication failed for [testuser@example.com (customAuthGuide Authentication failed, IP is not permitted for this user)]. Reason:  (customAuthGuide Authentication failed, IP is not permitted for this user).
2022-05-12 08:54:50,082 INFO  [qtp1335505684-116://localhost:8080/service/soap/BatchRequest] [name=testuser@example.com;oip=192.168.1.114;ua=zclient/9.0.0_GA_4258;soapId=72aefed5;] SoapEngine - handler exception: authentication failed for [testuser@example.com (customAuthGuide Authentication failed, IP is not permitted for this user)],  (customAuthGuide Authentication failed, IP is not permitted for this user)

, , ,

Comments are closed.

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