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}