001/* 002 * Copyright 2016-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2016-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2016-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk.unboundidds; 037 038 039 040import java.io.OutputStream; 041import java.io.Serializable; 042import java.util.LinkedHashMap; 043 044import com.unboundid.ldap.sdk.ExtendedResult; 045import com.unboundid.ldap.sdk.LDAPConnection; 046import com.unboundid.ldap.sdk.LDAPException; 047import com.unboundid.ldap.sdk.ResultCode; 048import com.unboundid.ldap.sdk.Version; 049import com.unboundid.ldap.sdk.unboundidds.extensions. 050 DeregisterYubiKeyOTPDeviceExtendedRequest; 051import com.unboundid.ldap.sdk.unboundidds.extensions. 052 RegisterYubiKeyOTPDeviceExtendedRequest; 053import com.unboundid.util.Debug; 054import com.unboundid.util.LDAPCommandLineTool; 055import com.unboundid.util.NotNull; 056import com.unboundid.util.Nullable; 057import com.unboundid.util.PasswordReader; 058import com.unboundid.util.StaticUtils; 059import com.unboundid.util.ThreadSafety; 060import com.unboundid.util.ThreadSafetyLevel; 061import com.unboundid.util.args.ArgumentException; 062import com.unboundid.util.args.ArgumentParser; 063import com.unboundid.util.args.BooleanArgument; 064import com.unboundid.util.args.FileArgument; 065import com.unboundid.util.args.StringArgument; 066 067import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*; 068 069 070 071/** 072 * This class provides a utility that may be used to register a YubiKey OTP 073 * device for a specified user so that it may be used to authenticate that user. 074 * Alternately, it may be used to deregister one or all of the YubiKey OTP 075 * devices that have been registered for the user. 076 * <BR> 077 * <BLOCKQUOTE> 078 * <B>NOTE:</B> This class, and other classes within the 079 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 080 * supported for use against Ping Identity, UnboundID, and 081 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 082 * for proprietary functionality or for external specifications that are not 083 * considered stable or mature enough to be guaranteed to work in an 084 * interoperable way with other types of LDAP servers. 085 * </BLOCKQUOTE> 086 */ 087@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 088public final class RegisterYubiKeyOTPDevice 089 extends LDAPCommandLineTool 090 implements Serializable 091{ 092 /** 093 * The serial version UID for this serializable class. 094 */ 095 private static final long serialVersionUID = 5705120716566064832L; 096 097 098 099 // Indicates that the tool should deregister one or all of the YubiKey OTP 100 // devices for the user rather than registering a new device. 101 @Nullable private BooleanArgument deregister; 102 103 // Indicates that the tool should interactively prompt for the static password 104 // for the user for whom the YubiKey OTP device is to be registered or 105 // deregistered. 106 @Nullable private BooleanArgument promptForUserPassword; 107 108 // The path to a file containing the static password for the user for whom the 109 // YubiKey OTP device is to be registered or deregistered. 110 @Nullable private FileArgument userPasswordFile; 111 112 // The username for the user for whom the YubiKey OTP device is to be 113 // registered or deregistered. 114 @Nullable private StringArgument authenticationID; 115 116 // The static password for the user for whom the YubiKey OTP device is to be 117 // registered or deregistered. 118 @Nullable private StringArgument userPassword; 119 120 // A one-time password generated by the YubiKey OTP device to be registered 121 // or deregistered. 122 @Nullable private StringArgument otp; 123 124 125 126 /** 127 * Parse the provided command line arguments and perform the appropriate 128 * processing. 129 * 130 * @param args The command line arguments provided to this program. 131 */ 132 public static void main(@NotNull final String... args) 133 { 134 final ResultCode resultCode = main(args, System.out, System.err); 135 if (resultCode != ResultCode.SUCCESS) 136 { 137 System.exit(resultCode.intValue()); 138 } 139 } 140 141 142 143 /** 144 * Parse the provided command line arguments and perform the appropriate 145 * processing. 146 * 147 * @param args The command line arguments provided to this program. 148 * @param outStream The output stream to which standard out should be 149 * written. It may be {@code null} if output should be 150 * suppressed. 151 * @param errStream The output stream to which standard error should be 152 * written. It may be {@code null} if error messages 153 * should be suppressed. 154 * 155 * @return A result code indicating whether the processing was successful. 156 */ 157 @NotNull() 158 public static ResultCode main(@NotNull final String[] args, 159 @Nullable final OutputStream outStream, 160 @Nullable final OutputStream errStream) 161 { 162 final RegisterYubiKeyOTPDevice tool = 163 new RegisterYubiKeyOTPDevice(outStream, errStream); 164 return tool.runTool(args); 165 } 166 167 168 169 /** 170 * Creates a new instance of this tool. 171 * 172 * @param outStream The output stream to which standard out should be 173 * written. It may be {@code null} if output should be 174 * suppressed. 175 * @param errStream The output stream to which standard error should be 176 * written. It may be {@code null} if error messages 177 * should be suppressed. 178 */ 179 public RegisterYubiKeyOTPDevice(@Nullable final OutputStream outStream, 180 @Nullable final OutputStream errStream) 181 { 182 super(outStream, errStream); 183 184 deregister = null; 185 otp = null; 186 promptForUserPassword = null; 187 userPasswordFile = null; 188 authenticationID = null; 189 userPassword = null; 190 } 191 192 193 194 /** 195 * {@inheritDoc} 196 */ 197 @Override() 198 @NotNull() 199 public String getToolName() 200 { 201 return "register-yubikey-otp-device"; 202 } 203 204 205 206 /** 207 * {@inheritDoc} 208 */ 209 @Override() 210 @NotNull() 211 public String getToolDescription() 212 { 213 return INFO_REGISTER_YUBIKEY_OTP_DEVICE_TOOL_DESCRIPTION.get( 214 UnboundIDYubiKeyOTPBindRequest.UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME); 215 } 216 217 218 219 /** 220 * {@inheritDoc} 221 */ 222 @Override() 223 @NotNull() 224 public String getToolVersion() 225 { 226 return Version.NUMERIC_VERSION_STRING; 227 } 228 229 230 231 /** 232 * {@inheritDoc} 233 */ 234 @Override() 235 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 236 throws ArgumentException 237 { 238 deregister = new BooleanArgument(null, "deregister", 1, 239 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_DEREGISTER.get("--otp")); 240 deregister.addLongIdentifier("de-register", true); 241 parser.addArgument(deregister); 242 243 otp = new StringArgument(null, "otp", false, 1, 244 INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_OTP.get(), 245 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_OTP.get()); 246 parser.addArgument(otp); 247 248 authenticationID = new StringArgument(null, "authID", false, 1, 249 INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_AUTHID.get(), 250 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_AUTHID.get()); 251 authenticationID.addLongIdentifier("authenticationID", true); 252 authenticationID.addLongIdentifier("auth-id", true); 253 authenticationID.addLongIdentifier("authentication-id", true); 254 parser.addArgument(authenticationID); 255 256 userPassword = new StringArgument(null, "userPassword", false, 1, 257 INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_USER_PW.get(), 258 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_USER_PW.get( 259 authenticationID.getIdentifierString())); 260 userPassword.setSensitive(true); 261 userPassword.addLongIdentifier("user-password", true); 262 parser.addArgument(userPassword); 263 264 userPasswordFile = new FileArgument(null, "userPasswordFile", false, 1, 265 null, 266 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_USER_PW_FILE.get( 267 authenticationID.getIdentifierString()), 268 true, true, true, false); 269 userPasswordFile.addLongIdentifier("user-password-file", true); 270 parser.addArgument(userPasswordFile); 271 272 promptForUserPassword = new BooleanArgument(null, "promptForUserPassword", 273 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_PROMPT_FOR_USER_PW.get( 274 authenticationID.getIdentifierString())); 275 promptForUserPassword.addLongIdentifier("prompt-for-user-password", true); 276 parser.addArgument(promptForUserPassword); 277 278 279 // At most one of the userPassword, userPasswordFile, and 280 // promptForUserPassword arguments must be present. 281 parser.addExclusiveArgumentSet(userPassword, userPasswordFile, 282 promptForUserPassword); 283 284 // If any of the userPassword, userPasswordFile, or promptForUserPassword 285 // arguments is present, then the authenticationID argument must also be 286 // present. 287 parser.addDependentArgumentSet(userPassword, authenticationID); 288 parser.addDependentArgumentSet(userPasswordFile, authenticationID); 289 parser.addDependentArgumentSet(promptForUserPassword, authenticationID); 290 } 291 292 293 294 /** 295 * {@inheritDoc} 296 */ 297 @Override() 298 public void doExtendedNonLDAPArgumentValidation() 299 throws ArgumentException 300 { 301 // If the deregister argument was not provided, then the otp argument must 302 // have been given. 303 if ((! deregister.isPresent()) && (! otp.isPresent())) 304 { 305 throw new ArgumentException( 306 ERR_REGISTER_YUBIKEY_OTP_DEVICE_NO_OTP_TO_REGISTER.get( 307 otp.getIdentifierString())); 308 } 309 } 310 311 312 313 /** 314 * {@inheritDoc} 315 */ 316 @Override() 317 public boolean supportsInteractiveMode() 318 { 319 return true; 320 } 321 322 323 324 /** 325 * {@inheritDoc} 326 */ 327 @Override() 328 public boolean defaultsToInteractiveMode() 329 { 330 return true; 331 } 332 333 334 335 /** 336 * {@inheritDoc} 337 */ 338 @Override() 339 protected boolean supportsOutputFile() 340 { 341 return true; 342 } 343 344 345 346 /** 347 * {@inheritDoc} 348 */ 349 @Override() 350 protected boolean defaultToPromptForBindPassword() 351 { 352 return true; 353 } 354 355 356 357 /** 358 * Indicates whether this tool supports the use of a properties file for 359 * specifying default values for arguments that aren't specified on the 360 * command line. 361 * 362 * @return {@code true} if this tool supports the use of a properties file 363 * for specifying default values for arguments that aren't specified 364 * on the command line, or {@code false} if not. 365 */ 366 @Override() 367 public boolean supportsPropertiesFile() 368 { 369 return true; 370 } 371 372 373 374 /** 375 * {@inheritDoc} 376 */ 377 @Override() 378 protected boolean supportsDebugLogging() 379 { 380 return true; 381 } 382 383 384 385 /** 386 * Indicates whether the LDAP-specific arguments should include alternate 387 * versions of all long identifiers that consist of multiple words so that 388 * they are available in both camelCase and dash-separated versions. 389 * 390 * @return {@code true} if this tool should provide multiple versions of 391 * long identifiers for LDAP-specific arguments, or {@code false} if 392 * not. 393 */ 394 @Override() 395 protected boolean includeAlternateLongIdentifiers() 396 { 397 return true; 398 } 399 400 401 402 /** 403 * Indicates whether this tool should provide a command-line argument that 404 * allows for low-level SSL debugging. If this returns {@code true}, then an 405 * "--enableSSLDebugging}" argument will be added that sets the 406 * "javax.net.debug" system property to "all" before attempting any 407 * communication. 408 * 409 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 410 * argument, or {@code false} if not. 411 */ 412 @Override() 413 protected boolean supportsSSLDebugging() 414 { 415 return true; 416 } 417 418 419 420 /** 421 * {@inheritDoc} 422 */ 423 @Override() 424 protected boolean logToolInvocationByDefault() 425 { 426 return true; 427 } 428 429 430 431 /** 432 * {@inheritDoc} 433 */ 434 @Override() 435 @NotNull() 436 public ResultCode doToolProcessing() 437 { 438 // Establish a connection to the Directory Server. 439 final LDAPConnection conn; 440 try 441 { 442 conn = getConnection(); 443 } 444 catch (final LDAPException le) 445 { 446 Debug.debugException(le); 447 wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 448 ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_CONNECT.get( 449 StaticUtils.getExceptionMessage(le))); 450 return le.getResultCode(); 451 } 452 453 try 454 { 455 // Get the authentication ID and static password to include in the 456 // request. 457 final String authID = authenticationID.getValue(); 458 459 final byte[] staticPassword; 460 if (userPassword.isPresent()) 461 { 462 staticPassword = StaticUtils.getBytes(userPassword.getValue()); 463 } 464 else if (userPasswordFile.isPresent()) 465 { 466 try 467 { 468 final char[] pwChars = getPasswordFileReader().readPassword( 469 userPasswordFile.getValue()); 470 staticPassword = StaticUtils.getBytes(new String(pwChars)); 471 } 472 catch (final Exception e) 473 { 474 Debug.debugException(e); 475 wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 476 ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get( 477 StaticUtils.getExceptionMessage(e))); 478 return ResultCode.LOCAL_ERROR; 479 } 480 } 481 else if (promptForUserPassword.isPresent()) 482 { 483 try 484 { 485 getOut().print(INFO_REGISTER_YUBIKEY_OTP_DEVICE_ENTER_PW.get(authID)); 486 staticPassword = PasswordReader.readPassword(); 487 } 488 catch (final Exception e) 489 { 490 Debug.debugException(e); 491 wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 492 ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get( 493 StaticUtils.getExceptionMessage(e))); 494 return ResultCode.LOCAL_ERROR; 495 } 496 } 497 else 498 { 499 staticPassword = null; 500 } 501 502 503 // Construct and process the appropriate register or deregister request. 504 if (deregister.isPresent()) 505 { 506 final DeregisterYubiKeyOTPDeviceExtendedRequest r = 507 new DeregisterYubiKeyOTPDeviceExtendedRequest(authID, 508 staticPassword, otp.getValue()); 509 510 ExtendedResult deregisterResult; 511 try 512 { 513 deregisterResult = conn.processExtendedOperation(r); 514 } 515 catch (final LDAPException le) 516 { 517 deregisterResult = new ExtendedResult(le); 518 } 519 520 if (deregisterResult.getResultCode() == ResultCode.SUCCESS) 521 { 522 if (otp.isPresent()) 523 { 524 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 525 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ONE.get( 526 authID)); 527 } 528 else 529 { 530 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 531 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ALL.get( 532 authID)); 533 } 534 return ResultCode.SUCCESS; 535 } 536 else 537 { 538 wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 539 ERR_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_FAILED.get(authID, 540 String.valueOf(deregisterResult))); 541 return deregisterResult.getResultCode(); 542 } 543 } 544 else 545 { 546 final RegisterYubiKeyOTPDeviceExtendedRequest r = 547 new RegisterYubiKeyOTPDeviceExtendedRequest(authID, staticPassword, 548 otp.getValue()); 549 550 ExtendedResult registerResult; 551 try 552 { 553 registerResult = conn.processExtendedOperation(r); 554 } 555 catch (final LDAPException le) 556 { 557 registerResult = new ExtendedResult(le); 558 } 559 560 if (registerResult.getResultCode() == ResultCode.SUCCESS) 561 { 562 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 563 INFO_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_SUCCESS.get(authID)); 564 return ResultCode.SUCCESS; 565 } 566 else 567 { 568 wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 569 ERR_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_FAILED.get(authID, 570 String.valueOf(registerResult))); 571 return registerResult.getResultCode(); 572 } 573 } 574 } 575 finally 576 { 577 conn.close(); 578 } 579 } 580 581 582 583 /** 584 * {@inheritDoc} 585 */ 586 @Override() 587 @NotNull() 588 public LinkedHashMap<String[],String> getExampleUsages() 589 { 590 final LinkedHashMap<String[],String> exampleMap = 591 new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); 592 593 String[] args = 594 { 595 "--hostname", "server.example.com", 596 "--port", "389", 597 "--bindDN", "uid=admin,dc=example,dc=com", 598 "--bindPassword", "adminPassword", 599 "--authenticationID", "u:test.user", 600 "--userPassword", "testUserPassword", 601 "--otp", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr" 602 }; 603 exampleMap.put(args, 604 INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_REGISTER.get()); 605 606 args = new String[] 607 { 608 "--hostname", "server.example.com", 609 "--port", "389", 610 "--bindDN", "uid=admin,dc=example,dc=com", 611 "--bindPassword", "adminPassword", 612 "--deregister", 613 "--authenticationID", "dn:uid=test.user,ou=People,dc=example,dc=com" 614 }; 615 exampleMap.put(args, 616 INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_DEREGISTER.get()); 617 618 return exampleMap; 619 } 620}