Greetings!

We did a small thing and thought it may be of interest to a broader
audience.
One of our clients insisted in additional security using a second
factor authorization, using ENTRUST.

ENTRUST application would run on the users phone and generate a One
Time Token (8 digits), valid for 30 seconds.

When authenticating with username and password, this token must be
passed and verified. Else authentication shall be denied.
ActiveDirectory and JDBC Realms are in use.

To make this happen, we have implemented a `TokenAware` interface,
which the common realms can implement. It takes care of the 2FA part
and I have the examples attached.

1) in the shiro.ini change
the ActiveDirectoryRealm into TokenAwareActiveDirectoryRealm

2) then set the
properties tokenRegex, soapUrl, soapUser, soapPass, soapWs

# define the 2FA Token as 8 trailing digits
realm.tokenRegex = (.+)(\d{8})$

realm.soapUrl = https://172.20.236.28:7851/entrust2/service
realm.soapUser = ....
realm.soapPass = ....
realm.soapWs = http://ws.waei.uba.com/


3) then the User will need to append the 8 digit Entrust Token to the
regular password, e.g.

username: andr...@manticore-projects.com
password: MySecret25

would become

username: andr...@manticore-projects.com
password: MySecret2543218765

(With "43218765" obtained from the Entrust App, which will be valid one
time and for 30 seconds only.)

This works like a charm for us and has the advantage, that in can be
switched on/off just by changing the realm and works with existing
Forms and Dialogs (not depending on an extra pop-up or Token field).

The Code is attached, please do let me know what you think and if there
is any interest to commit this into the project.

All the best and cheers
Andreas


package com.manticore.common.auth;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public interface TokenAware {
    Logger log = LoggerFactory.getLogger(TokenAware.class);
    Map<String, String> properties = new HashMap<>();

    // Setters to allow injection from Shiro.ini
    default void setTokenRegex(String regex) {
        if (log.isTraceEnabled()) {
            log.trace("Apply Token Pattern:" + regex);
        }
        properties.put("tokenPattern", regex);
    }

    default void setSoapUrl(String soapUrl) {
        if (log.isTraceEnabled()) {
            log.trace("Apply SOAP Url:" + soapUrl);
        }
        properties.put("soapUrl", soapUrl);
    }

    default void setSoapUser(String soapUser) {
        if (log.isTraceEnabled()) {
            log.trace("Apply SOAP User:" + soapUser);
        }
        properties.put("soapUser", soapUser);
    }

    default void setSoapPass(String soapPass) {
        if (log.isTraceEnabled()) {
            log.trace("Apply SOAP Password (hidden).");
        }
        properties.put("soapPass", soapPass);
    }

    default void setSoapWs(String soapWs) {
        if (log.isTraceEnabled()) {
            log.trace("Apply SOAP WS:" + soapWs);
        }
        properties.put("soapWs", soapWs);
    }

    default String[] getAuthenticationTokenParts(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        String passwordWithToken = String.copyValueOf( (char[]) token.getCredentials());

        Pattern tokenPattern = Pattern.compile(properties.get("tokenPattern"));
        Matcher matcher = tokenPattern.matcher(passwordWithToken);
        if (!matcher.matches()) {
            throw new AuthenticationException(
                    "Invalid password format: Does not match match " + tokenPattern.pattern()
            );
        }
        String password = matcher.group(1);
        String tokenPart = matcher.group(2);

        return new String[]{ username, password, tokenPart};
    }

    default boolean validateToken(String username, String token) {
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpPost request = new HttpPost(properties.get("soapUrl"));
            request.setHeader("Content-Type", "text/xml; charset=utf-8");
            request.setHeader("SOAPAction", "authenticateToken");
            request.setHeader("Authorization", "Basic " +
                    Base64.getEncoder().encodeToString(
                                    (properties.get("soapUser") + ":" + properties.get("soapPass"))
                                    .getBytes(StandardCharsets.UTF_8)
                    )
            );

            String soapRequest = buildSOAPRequest(properties.get("soapWs"), username, token);
            if (log.isTraceEnabled()) {
                log.trace("Sending Soap Token Request:\n" + soapRequest);
            }

            request.setEntity(new StringEntity(soapRequest));

            try (CloseableHttpResponse response = client.execute(request)) {
                String xmlResponse = EntityUtils.toString(response.getEntity());
                if (log.isTraceEnabled()) {
                    log.trace("Got response:\n" + xmlResponse);
                }
                return parseIsSuccessful(xmlResponse);
            }
        } catch (Exception e) {
            log.error("Failed to validate the token", e);
            return false;
        }
    }

    default boolean parseIsSuccessful(String xmlResponse) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes(StandardCharsets.UTF_8)));

            NodeList nodeList = document.getElementsByTagName("isSuccessful");
            if (nodeList.getLength() > 0) {
                return Boolean.parseBoolean(nodeList.item(0).getTextContent().trim());
            }
        } catch (Exception e) {
            log.error("Failed to parse the response", e);
        }
        return false;
    }

    default String buildSOAPRequest(String soapWs, String username, String token) {
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"\n"; +
                "                  xmlns:ws=\"" + soapWs + "\">\n" +
                "   <soapenv:Header/>\n" +
                "   <soapenv:Body>\n" +
                "      <ws:authenticateToken>\n" +
                "         <request>\n" +
                "            <response>" + token + "</response>\n" +
                "            <userGroup></userGroup>\n" +
                "            <username>" + username + "</username>\n" +
                "            <requesterId>?</requesterId>\n" +
                "            <requesterIp>?</requesterIp>\n" +
                "         </request>\n" +
                "      </ws:authenticateToken>\n" +
                "   </soapenv:Body>\n" +
                "</soapenv:Envelope>";
    }
}
package com.manticore.common.auth;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.authc.credential.CredentialsMatcher;

public class TokenAwareMJdbcRealm extends MJdbcRealm implements TokenAware {
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String[] authenticationTokenParts = getAuthenticationTokenParts(token);
        String username = authenticationTokenParts[0];
        String password = authenticationTokenParts[1];

        // Authenticate against the Realm
        return super.doGetAuthenticationInfo(new UsernamePasswordToken(username, password));
    }

    @Override
    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        String[] authenticationParts = getAuthenticationTokenParts(token);
        String username = authenticationParts[0];
        String password = authenticationParts[1];
        String tokenPart = authenticationParts[2];

        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);

        CredentialsMatcher cm = this.getCredentialsMatcher();
        if (cm != null) {
            if (!cm.doCredentialsMatch(usernamePasswordToken, info)) {
                String msg = "Submitted credentials for token [" + usernamePasswordToken + "] did not match the expected credentials.";
                throw new IncorrectCredentialsException(msg);
            }

            // 2nd Factor Authorization
            if (!validateToken(username, tokenPart)) {
                throw new AuthenticationException("Token authentication failed.");
            }
        } else {
            throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication.  If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
        }
    }

}
package com.manticore.common.auth;

import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.realm.ldap.DefaultLdapRealm;

public class TokenAwareLdapRealm extends DefaultLdapRealm implements TokenAware {
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String[] authenticationTokenParts = getAuthenticationTokenParts(token);
        String username = authenticationTokenParts[0];
        String password = authenticationTokenParts[1];

        // Authenticate against the Realm
        return super.doGetAuthenticationInfo(new UsernamePasswordToken(username, password));
    }

    @Override
    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        String[] authenticationParts = getAuthenticationTokenParts(token);
        String username = authenticationParts[0];
        String password = authenticationParts[1];
        String tokenPart = authenticationParts[2];

        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);

        CredentialsMatcher cm = this.getCredentialsMatcher();
        if (cm != null) {
            if (!cm.doCredentialsMatch(usernamePasswordToken, info)) {
                String msg = "Submitted credentials for token [" + usernamePasswordToken + "] did not match the expected credentials.";
                throw new IncorrectCredentialsException(msg);
            }

            // 2nd Factor Authorization
            if (!validateToken(username, tokenPart)) {
                throw new AuthenticationException("Token authentication failed.");
            }
        } else {
            throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication.  If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
        }
    }
}
package com.manticore.common.auth;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.realm.activedirectory.ActiveDirectoryRealm;

public class TokenAwareActiveDirectoryRealm extends ActiveDirectoryRealm implements TokenAware {
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String[] authenticationTokenParts = getAuthenticationTokenParts(token);
        String username = authenticationTokenParts[0];
        String password = authenticationTokenParts[1];

        // Authenticate against the Realm
        return super.doGetAuthenticationInfo(new UsernamePasswordToken(username, password));
    }

    @Override
    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        String[] authenticationParts = getAuthenticationTokenParts(token);
        String username = authenticationParts[0];
        String password = authenticationParts[1];
        String tokenPart = authenticationParts[2];

        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);

        CredentialsMatcher cm = this.getCredentialsMatcher();
        if (cm != null) {
            if (!cm.doCredentialsMatch(usernamePasswordToken, info)) {
                String msg = "Submitted credentials for token [" + usernamePasswordToken + "] did not match the expected credentials.";
                throw new IncorrectCredentialsException(msg);
            }

            // 2nd Factor Authorization
            if (!validateToken(username, tokenPart)) {
                throw new AuthenticationException("Token authentication failed.");
            }
        } else {
            throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication.  If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
        }
    }
}

Reply via email to