001package com.pingidentity.ds.plugin; 002 003import com.unboundid.directory.sdk.common.api.MonitorProvider; 004import com.unboundid.directory.sdk.common.operation.UpdatableBindResult; 005import com.unboundid.directory.sdk.common.operation.UpdatableSimpleBindRequest; 006import com.unboundid.directory.sdk.common.types.ActiveOperationContext; 007import com.unboundid.directory.sdk.common.types.RegisteredMonitorProvider; 008import com.unboundid.directory.sdk.ds.api.Plugin; 009import com.unboundid.directory.sdk.ds.config.PluginConfig; 010import com.unboundid.directory.sdk.ds.types.DirectoryServerContext; 011import com.unboundid.directory.sdk.ds.types.PreParsePluginResult; 012import com.unboundid.ldap.sdk.*; 013import com.unboundid.util.ByteString; 014import com.unboundid.util.args.ArgumentException; 015import com.unboundid.util.args.ArgumentParser; 016import com.unboundid.util.args.IntegerArgument; 017import com.unboundid.util.args.StringArgument; 018import com.yubico.client.v2.VerificationResponse; 019import com.yubico.client.v2.YubicoClient; 020import com.yubico.client.v2.exceptions.YubicoValidationFailure; 021import com.yubico.client.v2.exceptions.YubicoVerificationException; 022 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.List; 026import java.util.concurrent.atomic.AtomicLong; 027 028public class YubiOnSimpleBind extends Plugin { 029 public static final String ARG_CLIENT_ID_NAME = "client-id"; 030 public static final String ARG_CLIENT_SECRET_NAME = "client-secret"; 031 public static final String ARG_PUBLIC_ID_ATTR_NAME = "public-id-attribute"; 032 public static final String ARG_PUBLIC_ID_ATTR_DEFAULT = "ds-auth-yubikey-public-id"; 033 public static final int YUBI_OTP_LENGTH = 44; 034 DirectoryServerContext serverContext; 035 PluginConfig config; 036 private RegisteredMonitorProvider registeredMonitorProvider; 037 private Integer yubiClientID; 038 private String yubiClientSecret; 039 private String yubiPublicIDAttribute; 040 private YubicoClient yubiClient; 041 AtomicLong evaluatedRequests; 042 AtomicLong processedRequests; 043 AtomicLong successfulRequests; 044 045 046 047 @Override 048 public String getExtensionName() { 049 return "ds-plugin-yubi-on-simple-bind"; 050 } 051 052 @Override 053 public String[] getExtensionDescription() { 054 return new String[]{"Provides support for authenticating with Yubikey One-Time Passwords over Simple BIND Requests.", 055 "To enable this extension, you must first enable the Yubikey SASL Mechanism<br/>\n" + 056 "<pre>\n" + 057 "dsconfig set-sasl-mechanism-handler-prop \n" + 058 " --handler-name UNBOUNDID-YUBIKEY-OTP \n" + 059 " --set yubikey-client-id:12345 \n" + 060 " --set yubikey-api-key:CHANGEME \n" + 061 " --set enabled:true\n" + 062 "</pre>", 063 "Once this pre-requisite is met, you can enable the plugin with:\n" + 064 "<pre>\n" + 065 "dsconfig create-plugin \n" + 066 " --plugin-name yubi-on-simple-bind \n" + 067 " --type third-party \n" + 068 " --set enabled:false \n" + 069 " --set plugin-type:preparsebind \n" + 070 " --set invoke-for-internal-operations:false \n" + 071 " --set extension-class:com.pingidentity.ds.plugin.YubiOnSimpleBind \n" + 072 " --set extension-argument:client-id=12345 \n" + 073 " --set extension-argument:client-secret=CHANGEME\n" + 074 "</pre>", 075 "You may register a key with the register-yubikey-otp-device, like:\n" + 076 "<pre>\n" + 077 "register-yubikey-otp-device \n" + 078 " --authenticationID u:user.0 \n" + 079 " --userPassword password \n" + 080 " --otp SOMEOTPHERE\n" + 081 "</pre>", 082 "Finally you can try out that authentication now requires yubikey otp for users with registered devices:<br/>\n" + 083 "<pre>\n" + 084 "bin/ldapsearch \n" + 085 " -D uid=user.0,ou=People,dc=example,dc=com \n" + 086 " -w passwordSOMEOTPHERE \n" + 087 " -b uid=user.0,ou=People,dc=example,dc=com \n" + 088 " -s base \n" + 089 " '(&)'\n" + 090 "</pre>", 091 "NOTE: On PingDirectoryProxy, it is necessary to specify the public-id-attribute argument for which the user has read rights in the back-end server."}; 092 } 093 094 @Override 095 public void defineConfigArguments(ArgumentParser parser) throws ArgumentException { 096 IntegerArgument yubiClientArg = new IntegerArgument(null, ARG_CLIENT_ID_NAME, true, 1, "{"+ ARG_CLIENT_ID_NAME +"}", "The YubiCo client ID"); 097 parser.addArgument(yubiClientArg); 098 099 StringArgument yubiSecretArg = new StringArgument(null, ARG_CLIENT_SECRET_NAME, true, 1, "{"+ARG_CLIENT_SECRET_NAME+"}", "The secret key for the client"); 100 parser.addArgument(yubiSecretArg); 101 102 StringArgument yubiPublicAttrArg = new StringArgument(null, ARG_PUBLIC_ID_ATTR_NAME,false,1,"{"+ARG_PUBLIC_ID_ATTR_NAME+"}","The name of the attribute to ", ARG_PUBLIC_ID_ATTR_DEFAULT); 103 parser.addArgument(yubiPublicAttrArg); 104 } 105 106 @Override 107 public ResultCode applyConfiguration(PluginConfig config, ArgumentParser parser, List<String> adminActionsRequired, List<String> messages) { 108 this.config = config; 109 yubiClientID = parser.getIntegerArgument(ARG_CLIENT_ID_NAME).getValue(); 110 yubiClientSecret = parser.getStringArgument(ARG_CLIENT_SECRET_NAME).getValue(); 111 yubiPublicIDAttribute =parser.getStringArgument(ARG_PUBLIC_ID_ATTR_NAME).getValue(); 112 yubiClient = YubicoClient.getClient(yubiClientID, yubiClientSecret); 113 return ResultCode.SUCCESS; 114 } 115 116 @Override 117 public void initializePlugin(DirectoryServerContext serverContext, PluginConfig config, ArgumentParser parser) throws LDAPException { 118 this.serverContext = serverContext; 119 List<String> adminActionMessages = new ArrayList<>(3); 120 List<String> messages = new ArrayList<>(3); 121 ResultCode resultCode = applyConfiguration(config, parser, adminActionMessages, messages); 122 if (!ResultCode.SUCCESS.equals(resultCode)) { 123 adminActionMessages.forEach(s -> System.out.println(s)); 124 messages.forEach(s -> System.out.println(s)); 125 } 126 evaluatedRequests = new AtomicLong(0); 127 processedRequests = new AtomicLong(0); 128 successfulRequests = new AtomicLong(0); 129 registeredMonitorProvider = serverContext.registerMonitorProvider(new YubiOnSimpleBindMonitorProvider(),config); 130 } 131 132 @Override 133 public void finalizePlugin() { 134 if ( registeredMonitorProvider != null) 135 serverContext.deregisterMonitorProvider(registeredMonitorProvider); 136 } 137 138 @Override 139 public PreParsePluginResult doPreParse(ActiveOperationContext operationContext, UpdatableSimpleBindRequest request, UpdatableBindResult result) { 140 PreParsePluginResult fastInterrupt = new PreParsePluginResult(false, false, true, true); 141 142 if (request == null ){ 143 result.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 144 result.setDiagnosticMessage("No request received"); 145 return fastInterrupt; 146 } 147 148 ByteString pwdByteString = request.getPassword(); 149 if ( pwdByteString == null ){ 150 result.setResultCode(ResultCode.INVALID_CREDENTIALS); 151 result.setDiagnosticMessage("no password received"); 152 return fastInterrupt; 153 } 154 155 String pwdString = pwdByteString.stringValue(); 156 if ( pwdString.isEmpty() ){ 157 result.setResultCode(ResultCode.INVALID_CREDENTIALS); 158 result.setDiagnosticMessage("Received empty password"); 159 return fastInterrupt; 160 } 161 162 if (pwdString.length() < 44 ){ 163 result.setResultCode(ResultCode.INVALID_CREDENTIALS); 164 result.setDiagnosticMessage("Received password is too short to contain the user password followed by a Yubikey OTP"); 165 return fastInterrupt; 166 } 167 168 byte[] pwdByteArray = pwdByteString.getValue(); 169 byte[] yubiOTPByteArray = Arrays.copyOfRange(pwdByteArray, pwdByteArray.length - YUBI_OTP_LENGTH, pwdByteArray.length); 170 String yubiOTPString = new String(yubiOTPByteArray); 171 if ( ! YubicoClient.isValidOTPFormat(yubiOTPString) ){ 172 result.setResultCode(ResultCode.INVALID_CREDENTIALS); 173 result.setDiagnosticMessage("Invalid Yubikey OTP format"); 174 return fastInterrupt; 175 } 176 177 try { 178 VerificationResponse yubiResponse = yubiClient.verify(yubiOTPString); 179 if ( ! yubiResponse.isOk() ){ 180 result.setResultCode(ResultCode.INVALID_CREDENTIALS); 181 result.setDiagnosticMessage(String.valueOf(yubiResponse.getStatus())); 182 return fastInterrupt; 183 } 184 } catch (YubicoVerificationException e) { 185 result.setResultCode(ResultCode.OTHER); 186 result.setDiagnosticMessage(e.getMessage()); 187 e.printStackTrace(); 188 return fastInterrupt; 189 } catch (YubicoValidationFailure yubicoValidationFailure) { 190 result.setResultCode(ResultCode.OTHER); 191 result.setDiagnosticMessage(yubicoValidationFailure.getMessage()); 192 yubicoValidationFailure.printStackTrace(); 193 return fastInterrupt; 194 } 195 196 try { 197 SearchResultEntry entry; 198 199 if ( serverContext.isDirectoryProxyFunctionalityAvailable() ) { 200 entry = operationContext.getInternalUserConnection().getEntry(request.getDN(), yubiPublicIDAttribute); 201 } else { 202 entry = operationContext.getInternalRootConnection().getEntry(request.getDN(),yubiPublicIDAttribute); 203 } 204 205 if ( entry == null ) { 206 result.setResultCode(ResultCode.INVALID_CREDENTIALS); 207 return fastInterrupt; 208 } 209 Attribute publicIDAttribute = entry.getAttribute(yubiPublicIDAttribute); 210 if (publicIDAttribute == null || !publicIDAttribute.hasValue()){ 211 result.setResultCode(ResultCode.INVALID_CREDENTIALS); 212 return fastInterrupt; 213 } 214 String submittedPublicID = YubicoClient.getPublicId(yubiOTPString); 215 // multiple yubikeys might be registered for a user 216 Boolean publicIDFound = Boolean.FALSE; 217 for ( String registeredPublicID: publicIDAttribute.getValues() ){ 218 if (submittedPublicID.equals(registeredPublicID)){ 219 publicIDFound=Boolean.TRUE; 220 break; 221 } 222 } 223 if ( ! publicIDFound ){ 224 result.setResultCode(ResultCode.INVALID_CREDENTIALS); 225 result.setDiagnosticMessage("Yubikey not registered with user"); 226 return fastInterrupt; 227 } 228 229 } catch (LDAPException e) { 230 result.setResultData(e); 231 return fastInterrupt; 232 } 233 234 byte[] simplePasswordByteArray = Arrays.copyOfRange(pwdByteArray, 0, pwdByteArray.length - YUBI_OTP_LENGTH); 235 String simplePasswordString = new String(simplePasswordByteArray); 236 237 request.setPassword(simplePasswordString); 238 return PreParsePluginResult.SUCCESS; 239 } 240 241 class YubiOnSimpleBindMonitorProvider extends MonitorProvider { 242 YubiOnSimpleBindMonitorProvider(){ 243 } 244 245 @Override 246 public String getExtensionName() { 247 return "ds-plugin-yubi-on-simple-bind-monitor-provider"; 248 } 249 250 @Override 251 public String[] getExtensionDescription() { 252 return null; 253 } 254 255 @Override 256 public String getMonitorInstanceName() { 257 return config.getConfigObjectName()+"-monitor"; 258 } 259 260 @Override 261 public String getMonitorObjectClass() { 262 return "ds-yubi-monitor-entry"; 263 } 264 265 @Override 266 public List<Attribute> getMonitorAttributes() { 267 List<Attribute> attributes = new ArrayList<>(3); 268 attributes.add(new Attribute("requests-evaluated", evaluatedRequests.toString())); 269 attributes.add(new Attribute("requests-processed", processedRequests.toString())); 270 attributes.add(new Attribute("requests-successful", successfulRequests.toString())); 271 return attributes; 272 } 273 } 274}