001package com.pingidentity.proxy.transformation;
002
003import com.pingidentity.developer.pingid.Application;
004import com.pingidentity.developer.pingid.Operation;
005import com.unboundid.directory.sdk.proxy.api.ProxyTransformation;
006import com.unboundid.directory.sdk.proxy.config.ProxyTransformationConfig;
007import com.unboundid.directory.sdk.proxy.types.ProxyOperationContext;
008import com.unboundid.directory.sdk.proxy.types.ProxyServerContext;
009import com.unboundid.ldap.sdk.*;
010import com.unboundid.util.args.*;
011
012import java.io.File;
013import java.io.FileInputStream;
014import java.io.IOException;
015import java.net.MalformedURLException;
016import java.net.URL;
017import java.util.*;
018
019public class PingIDOnSimpleBind extends ProxyTransformation {
020    private static final String ARG_TOTP_LENGTH = "totp-length";
021    private static final String ARG_TOTP_LAST = "totp-is-last";
022    private static final String ARG_TOTP_ENABLED = "totp-enabled";
023    private static final String ARG_PUSH_ENABLED = "push-enabled";
024    private static final String ARG_SEPARATOR = "separator";
025    public static final String ARG_SEPARATOR_DEFAULT = ",";
026    private static final Integer ARG_RESULT_CODE_DEFAULT = 49;
027    private static final String ARG_RESULT_CODE = "result-code";
028    private static final String ARG_RESULT_MESSAGE_DEFAULT = "Invalid authentication request for PingID MFA";
029    private static final String ARG_RESULT_MESSAGE = "result-message";
030    public static final int ARG_TOTP_LENGTH_DEFAULT = 6;
031    private static final String ARG_PUSH_MARKER_DEFAULT = "push";
032    private static final String ARG_PUSH_MARKER = "push-marker";
033    public static final String ARG_PINGID_PROPERTIES_FILE = "pingid-properties-file";
034    public static final String ARG_USER_ATTRIBUTE_DEFAULT = "mail";
035    private static final String ARG_USERNAME_ATTRIBUTE = "pingid-username-attribute";
036    public static final String PROP_ORG_ALIAS = "org_alias";
037    public static final String PROP_TOKEN = "token";
038    public static final String PROP_BASE64_KEY = "use_base64_key";
039    public static final String PROP_IDP_URL = "idp_url";
040    private static final String ARG_APPLICATION_NAME_DEFAULT = "ldap";
041    private static final String ARG_APPLICATION_NAME = "app-name";
042
043    public static final String ARG_AUTH_TYPE_CONFIRM = "CONFIRM";
044    public static final String ARG_AUTH_TYPE_PERMISSIVE = "FINGERPRINT_PERMISSIVE";
045    public static final String ARG_AUTH_TYPE_RESTRICTIVE = "FINGERPRINT_RESTRICTIVE";
046    public static final String ARG_AUTH_TYPE_HARD = "FINGERPRINT_HARD_RESTRICTIVE";
047    private static final String ARG_AUTH_TYPE = "auth-type";
048
049    private static final String ATTACHMENT_PROVIDED_PASSWORD = "providedPassword";
050
051
052    private Integer totpLength;
053    private Boolean totpIsSuffix;
054    private Boolean totpEnabled;
055    private String separator;
056    private Boolean pushEnabled;
057    private ResultCode resultCode;
058    private String message;
059    private String pushMarker;
060    private File propertiesFile;
061    private String pingidUsernameAttribute;
062    private Operation pingidOperation;
063    private String authType;
064    private Application application;
065    private ProxyServerContext serverContext;
066
067    @Override
068    public String getExtensionName() {
069        return "PingIDOnSimpleBind";
070    }
071
072    @Override
073    public String[] getExtensionDescription() {
074        return new String[]{"Provides a mechanism to intercept a simple BIND and use PingID for MFA"};
075    }
076
077    @Override
078    public void toString(StringBuilder stringBuilder) {
079    }
080
081    @Override
082    public void defineConfigArguments(ArgumentParser parser)
083            throws ArgumentException {
084        parser.addArgument(new IntegerArgument(null, ARG_TOTP_LENGTH, false, 1, "{length}", "TOTP code length(Default: " + ARG_TOTP_LENGTH_DEFAULT + ")", ARG_TOTP_LENGTH_DEFAULT));
085        parser.addArgument(new BooleanValueArgument(null, ARG_TOTP_LAST, false, "{boolean}", "Indicates that the TOTP code should be appended after the password", Boolean.TRUE));
086        parser.addArgument(new BooleanValueArgument(null, ARG_TOTP_ENABLED, false, "{boolean}", "Indicates that PingID TOTP code is enabled", Boolean.TRUE));
087        parser.addArgument(new BooleanValueArgument(null, ARG_PUSH_ENABLED, false, "{boolean}", "Indicates that the PingID push is enabled", Boolean.TRUE));
088        parser.addArgument(new StringArgument(null, ARG_SEPARATOR, false, 1, "{separator}", "The separator between the password to use for simple BIND authentication and the PingID marker(Default: " + ARG_SEPARATOR_DEFAULT + ")", ARG_SEPARATOR_DEFAULT));
089        parser.addArgument(new IntegerArgument(null, ARG_RESULT_CODE, false, 1, "{code}", "The result code to return to the client upon failure (Default: " + ARG_RESULT_CODE_DEFAULT + ")", ARG_RESULT_CODE_DEFAULT));
090        parser.addArgument(new StringArgument(null, ARG_RESULT_MESSAGE, false, 1, "{message}", "The result message to return to the client upon failure (Default: " + ARG_RESULT_MESSAGE_DEFAULT + ")", ARG_RESULT_MESSAGE_DEFAULT));
091        parser.addArgument(new StringArgument(null, ARG_PUSH_MARKER, false, 1, "{push-marker}", "The marker to use to indicate to the plugin to initiate a push notification (Default: " + ARG_PUSH_MARKER_DEFAULT + ")", ARG_PUSH_MARKER_DEFAULT));
092        parser.addArgument(new FileArgument(null, ARG_PINGID_PROPERTIES_FILE, true, 1, "{file}", "Path to the properties files containing the PingID connection settings"));
093        parser.addArgument(new StringArgument(null, ARG_APPLICATION_NAME, false, 1, "{app-name}", "The application name (Default: " + ARG_APPLICATION_NAME_DEFAULT + ")", ARG_APPLICATION_NAME_DEFAULT));
094        parser.addArgument(new StringArgument(null, ARG_AUTH_TYPE, false, 1, "{auth-type}", "The type of online authentication (Default:" + ARG_AUTH_TYPE_CONFIRM + ")", new HashSet<>(Arrays.asList(new String[]{ARG_AUTH_TYPE_CONFIRM, ARG_AUTH_TYPE_PERMISSIVE, ARG_AUTH_TYPE_RESTRICTIVE, ARG_AUTH_TYPE_HARD})), ARG_AUTH_TYPE_CONFIRM));
095        StringArgument argUsernameAttribute = new StringArgument(null, ARG_USERNAME_ATTRIBUTE, false, 1, "{attribute}", "The attribute to lookup in the user entry after successful simple BIND to get the PingID user name (Default:" + ARG_USER_ATTRIBUTE_DEFAULT + ")", ARG_USER_ATTRIBUTE_DEFAULT);
096        argUsernameAttribute.addValueValidator(new AttributeNameArgumentValueValidator());
097        parser.addArgument(argUsernameAttribute);
098    }
099
100    @Override
101    public ResultCode applyConfiguration(ProxyTransformationConfig config, ArgumentParser parser, List<String> adminActionsRequired, List<String> messages) {
102        serverContext = config.getServerContext();
103        totpLength = parser.getIntegerArgument(ARG_TOTP_LENGTH).getValue();
104        totpIsSuffix = parser.getBooleanValueArgument(ARG_TOTP_LAST).getValue();
105        totpEnabled = parser.getBooleanValueArgument(ARG_TOTP_ENABLED).getValue();
106        pushEnabled = parser.getBooleanValueArgument(ARG_PUSH_ENABLED).getValue();
107        separator = parser.getStringArgument(ARG_SEPARATOR).getValue();
108        resultCode = ResultCode.valueOf(parser.getIntegerArgument(ARG_RESULT_CODE).getValue());
109        message = parser.getStringArgument(ARG_RESULT_MESSAGE).getValue();
110        pushMarker = parser.getStringArgument(ARG_PUSH_MARKER).getValue();
111        propertiesFile = parser.getFileArgument(ARG_PINGID_PROPERTIES_FILE).getValue();
112        pingidUsernameAttribute = parser.getStringArgument(ARG_USERNAME_ATTRIBUTE).getValue();
113        application = new Application(parser.getStringArgument(ARG_APPLICATION_NAME).getValue());
114        authType = parser.getStringArgument(ARG_AUTH_TYPE).getValue();
115        Properties properties = new Properties();
116        try {
117            properties.load(new FileInputStream(propertiesFile));
118            String orgAlias = properties.getProperty(PROP_ORG_ALIAS);
119            String token = properties.getProperty(PROP_TOKEN);
120            String key = properties.getProperty(PROP_BASE64_KEY);
121            String url = properties.getProperty(PROP_IDP_URL);
122            pingidOperation = new Operation(orgAlias, token, key, url);
123        } catch (IOException e) {
124            messages.add("Could not load " + propertiesFile.getPath());
125            return ResultCode.OTHER;
126        }
127        return ResultCode.SUCCESS;
128    }
129
130    @Override
131    public void initializeProxyTransformation(ProxyServerContext serverContext, ProxyTransformationConfig config, ArgumentParser parser) throws LDAPException {
132        super.initializeProxyTransformation(serverContext, config, parser);
133    }
134
135    @Override
136    public boolean isConfigurationAcceptable(ProxyTransformationConfig config, ArgumentParser parser, List<String> unacceptableReasons) {
137        if (unacceptableReasons == null) {
138            unacceptableReasons = new ArrayList<>(5);
139        }
140        int initialSize = unacceptableReasons.size();
141        propertiesFile = parser.getFileArgument(ARG_PINGID_PROPERTIES_FILE).getValue();
142        Properties properties = new Properties();
143        try {
144            properties.load(new FileInputStream(propertiesFile));
145            for (String propertyName : new String[]{PROP_ORG_ALIAS, PROP_TOKEN, PROP_BASE64_KEY, PROP_IDP_URL}) {
146                String propertyValue = properties.getProperty(propertyName);
147                if (propertyValue == null || propertyValue.isEmpty()) {
148                    unacceptableReasons.add(propertyName + " must be provided and was not empty or absent from the provided properties file");
149                }
150            }
151            String url = properties.getProperty(PROP_IDP_URL);
152            URL pingURL = new URL(url);
153        } catch (MalformedURLException mue) {
154            unacceptableReasons.add(mue.getMessage());
155        } catch (IOException ioe) {
156            unacceptableReasons.add(ioe.getMessage());
157        }
158        if (unacceptableReasons.size() > initialSize) {
159            return false;
160        }
161        return true;
162    }
163
164    @Override
165    public BindRequest transformBindRequest(ProxyOperationContext operationContext, BindRequest bindRequest) throws LDAPException {
166        if (bindRequest instanceof SimpleBindRequest) {
167            SimpleBindRequest simpleBindRequest = (SimpleBindRequest) bindRequest;
168            String providedPassword = simpleBindRequest.getPassword().stringValue();
169            if (providedPassword != null && !providedPassword.isEmpty() && providedPassword.contains(separator)) {
170                String simplePassword = providedPassword.substring(0, providedPassword.lastIndexOf(separator));
171                operationContext.setAttachment(ATTACHMENT_PROVIDED_PASSWORD, providedPassword);
172                return new SimpleBindRequest(simpleBindRequest.getBindDN(), simplePassword, simpleBindRequest.getControls());
173            }
174        }
175        return bindRequest;
176    }
177
178    @Override
179    public BindResult transformBindResult(ProxyOperationContext operationContext, BindRequest bindRequest, BindResult bindResult) {
180        if (!ResultCode.SUCCESS.equals(bindResult.getResultCode())) {
181            return bindResult;
182        }
183        try {
184            String pingidUsername = operationContext.getServerContext().getClientRootConnection(true).getEntry(bindResult.getMatchedDN(), pingidUsernameAttribute).getAttributeValue(pingidUsernameAttribute);
185            if (pingidUsername == null || pingidUsername.isEmpty()) {
186                return new BindResult(new LDAPException(ResultCode.INVALID_CREDENTIALS, "This user does not have a PingID user name set"));
187            }
188
189            Operation operation = new Operation(pingidOperation);
190            operation.setTargetUser(pingidUsername);
191            String providedPassword = (String) operationContext.getAttachment(ATTACHMENT_PROVIDED_PASSWORD);
192            if (totpEnabled && providedPassword.length() > (totpLength + separator.length()) && providedPassword.matches(".*" + separator + "\\d{" + totpLength + "}$")) {
193                String code = providedPassword.substring(providedPassword.lastIndexOf(separator) + 1);
194                operation.AuthenticateOnline(application, "OTP");
195                if (operation.getWasSuccessful()) {
196                    operation.AuthenticateOffline(code);
197                }
198            } else if (pushEnabled && providedPassword.length() > (pushMarker.length() + separator.length()) && providedPassword.endsWith(separator + pushMarker)) {
199                operation.AuthenticateOnline(application, authType);
200            }
201            if (operation.getWasSuccessful()) {
202                return bindResult;
203            } else {
204                return new BindResult(new LDAPException(resultCode, operation.getErrorMsg()));
205            }
206        } catch (LDAPException e) {
207            return new BindResult(e);
208        }
209    }
210}