001/* 002 * Copyright 2015-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2015-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) 2015-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.controls; 037 038 039 040import java.util.ArrayList; 041import java.util.Collection; 042import java.util.Collections; 043import java.util.Iterator; 044import java.util.LinkedHashMap; 045import java.util.List; 046import java.util.Map; 047 048import com.unboundid.asn1.ASN1Boolean; 049import com.unboundid.asn1.ASN1Element; 050import com.unboundid.asn1.ASN1Integer; 051import com.unboundid.asn1.ASN1Null; 052import com.unboundid.asn1.ASN1OctetString; 053import com.unboundid.asn1.ASN1Sequence; 054import com.unboundid.ldap.sdk.Control; 055import com.unboundid.ldap.sdk.DecodeableControl; 056import com.unboundid.ldap.sdk.JSONControlDecodeHelper; 057import com.unboundid.ldap.sdk.LDAPException; 058import com.unboundid.ldap.sdk.LDAPResult; 059import com.unboundid.ldap.sdk.ResultCode; 060import com.unboundid.ldap.sdk.unboundidds.extensions.PasswordQualityRequirement; 061import com.unboundid.util.Debug; 062import com.unboundid.util.NotMutable; 063import com.unboundid.util.NotNull; 064import com.unboundid.util.Nullable; 065import com.unboundid.util.StaticUtils; 066import com.unboundid.util.ThreadSafety; 067import com.unboundid.util.ThreadSafetyLevel; 068import com.unboundid.util.json.JSONArray; 069import com.unboundid.util.json.JSONBoolean; 070import com.unboundid.util.json.JSONField; 071import com.unboundid.util.json.JSONNumber; 072import com.unboundid.util.json.JSONObject; 073import com.unboundid.util.json.JSONString; 074import com.unboundid.util.json.JSONValue; 075 076import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*; 077 078 079 080/** 081 * This class provides an implementation for a response control that can be 082 * returned by the server in the response for add, modify, and password modify 083 * requests that include the password validation details request control. This 084 * response control will provide details about the password quality requirements 085 * that are in effect for the operation and whether the password included in the 086 * request satisfies each of those requirements. 087 * <BR> 088 * <BLOCKQUOTE> 089 * <B>NOTE:</B> This class, and other classes within the 090 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 091 * supported for use against Ping Identity, UnboundID, and 092 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 093 * for proprietary functionality or for external specifications that are not 094 * considered stable or mature enough to be guaranteed to work in an 095 * interoperable way with other types of LDAP servers. 096 * </BLOCKQUOTE> 097 * <BR> 098 * This response control has an OID of 1.3.6.1.4.1.30221.2.5.41, a criticality 099 * of {@code false}, and a value with the provided encoding: 100 * <PRE> 101 * PasswordValidationDetailsResponse ::= SEQUENCE { 102 * validationResult CHOICE { 103 * validationDetails [0] SEQUENCE OF 104 * PasswordQualityRequirementValidationResult, 105 * noPasswordProvided [1] NULL, 106 * multiplePasswordsProvided [2] NULL, 107 * noValidationAttempted [3] NULL, 108 * ... }, 109 * missingCurrentPassword [3] BOOLEAN DEFAULT FALSE, 110 * mustChangePassword [4] BOOLEAN DEFAULT FALSE, 111 * secondsUntilExpiration [5] INTEGER OPTIONAL, 112 * ... } 113 * </PRE> 114 */ 115@NotMutable() 116@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 117public final class PasswordValidationDetailsResponseControl 118 extends Control 119 implements DecodeableControl 120{ 121 /** 122 * The OID (1.3.6.1.4.1.30221.2.5.41) for the password validation details 123 * response control. 124 */ 125 @NotNull public static final String PASSWORD_VALIDATION_DETAILS_RESPONSE_OID = 126 "1.3.6.1.4.1.30221.2.5.41"; 127 128 129 130 /** 131 * The BER type for the missing current password element. 132 */ 133 private static final byte TYPE_MISSING_CURRENT_PASSWORD = (byte) 0x83; 134 135 136 137 /** 138 * The BER type for the must change password element. 139 */ 140 private static final byte TYPE_MUST_CHANGE_PW = (byte) 0x84; 141 142 143 144 /** 145 * The BER type for the seconds until expiration element. 146 */ 147 private static final byte TYPE_SECONDS_UNTIL_EXPIRATION = (byte) 0x85; 148 149 150 151 /** 152 * The name of the field used to hold an additional information string in the 153 * JSON representation of this control. 154 */ 155 @NotNull private static final String 156 JSON_FIELD_ADDITIONAL_INFORMATION = "additional-information"; 157 158 159 160 /** 161 * The name of the field used to hold the set of client-side validation 162 * properties in the JSON representation of this control. 163 */ 164 @NotNull private static final String 165 JSON_FIELD_CLIENT_SIDE_VALIDATION_PROPERTIES = 166 "client-side-validation-properties"; 167 168 169 170 /** 171 * The name of the field used to hold a client-side validation type in the 172 * JSON representation of this control. 173 */ 174 @NotNull private static final String JSON_FIELD_CLIENT_SIDE_VALIDATION_TYPE = 175 "client-side-validation-type"; 176 177 178 179 /** 180 * The name of the field used to hold a description in the JSON representation 181 * of this control. 182 */ 183 @NotNull private static final String JSON_FIELD_DESCRIPTION = "description"; 184 185 186 187 /** 188 * The name of the field used to hold the missing current password flag in the 189 * JSON representation of this control. 190 */ 191 @NotNull private static final String JSON_FIELD_MISSING_CURRENT_PASSWORD = 192 "missing-current-password"; 193 194 195 196 /** 197 * The name of the field used to hold the must change password flag in the 198 * JSON representation of this control. 199 */ 200 @NotNull private static final String JSON_FIELD_MUST_CHANGE_PASSWORD = 201 "must-change-password"; 202 203 204 205 /** 206 * The name of the field used to hold a password quality requirement in the 207 * JSON representation of this control. 208 */ 209 @NotNull private static final String JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT = 210 "password-quality-requirement"; 211 212 213 214 /** 215 * The name of the field used to hold a property name in the JSON 216 * representation of this control. 217 */ 218 @NotNull private static final String JSON_FIELD_PROPERTY_NAME = "name"; 219 220 221 222 /** 223 * The name of the field used to hold a property value in the JSON 224 * representation of this control. 225 */ 226 @NotNull private static final String JSON_FIELD_PROPERTY_VALUE = "value"; 227 228 229 230 /** 231 * The name of the field used to indicate whether a password quality 232 * requirement was satisfied in the JSON representation of this control. 233 */ 234 @NotNull private static final String JSON_FIELD_REQUIREMENT_SATISFIED = 235 "requirement-satisfied"; 236 237 238 239 /** 240 * The name of the field used to hold the response type in the JSON 241 * representation of this control. 242 */ 243 @NotNull private static final String JSON_FIELD_RESPONSE_TYPE = 244 "response-type"; 245 246 247 248 /** 249 * The name of the field used to hold the seconds until expiration in the JSON 250 * representation of this control. 251 */ 252 @NotNull private static final String JSON_FIELD_SECONDS_UNTIL_EXPIRATION = 253 "seconds-until-expiration"; 254 255 256 257 /** 258 * The name of the field used to hold validation details objects in the JSON 259 * representation of this control. 260 */ 261 @NotNull private static final String JSON_FIELD_VALIDATION_DETAILS = 262 "validation-details"; 263 264 265 266 /** 267 * The multiple-passwords-provided response type value in the JSON 268 * representation of this control. 269 */ 270 @NotNull private static final String 271 JSON_RESPONSE_TYPE_MULTIPLE_PASSWORDS_PROVIDED = 272 "multiple-passwords-provided"; 273 274 275 276 /** 277 * The no-password-provided response type value in the JSON representation of 278 * this control. 279 */ 280 @NotNull private static final String JSON_RESPONSE_TYPE_NO_PASSWORD_PROVIDED = 281 "no-password-provided"; 282 283 284 285 /** 286 * The no-validation-attempted response type value in the JSON representation 287 * of this control. 288 */ 289 @NotNull private static final String 290 JSON_RESPONSE_TYPE_NO_VALIDATION_ATTEMPTED = "no-validation-attempted"; 291 292 293 294 /** 295 * The validation-performed response type value in the JSON representation of 296 * this control. 297 */ 298 @NotNull private static final String JSON_RESPONSE_TYPE_VALIDATION_PERFORMED = 299 "validation-performed"; 300 301 302 303 /** 304 * The serial version UID for this serializable class. 305 */ 306 private static final long serialVersionUID = -2205640814914704074L; 307 308 309 310 // Indicates whether the associated password self change operation failed 311 // (or would fail if attempted without validation errors) because the user is 312 // required to provide his/her current password when performing a self change 313 // but did not do so. 314 private final boolean missingCurrentPassword; 315 316 // Indicates whether the user will be required to change his/her password 317 // immediately after the associated add or administrative password reset is 318 // complete. 319 private final boolean mustChangePassword; 320 321 // The length of time in seconds that the new password will be considered 322 // valid. 323 @Nullable private final Integer secondsUntilExpiration; 324 325 // The list of the validation results for the associated operation. 326 @NotNull private final List<PasswordQualityRequirementValidationResult> 327 validationResults; 328 329 // The response type for this password validation details response control. 330 @NotNull private final PasswordValidationDetailsResponseType responseType; 331 332 333 334 /** 335 * Creates a new empty control instance that is intended to be used only for 336 * decoding controls via the {@code DecodeableControl} interface. 337 */ 338 PasswordValidationDetailsResponseControl() 339 { 340 responseType = null; 341 validationResults = null; 342 missingCurrentPassword = true; 343 mustChangePassword = true; 344 secondsUntilExpiration = null; 345 } 346 347 348 349 /** 350 * Creates a password validation details response control with the provided 351 * information. 352 * 353 * @param responseType The response type for this password 354 * validation details response control. This 355 * must not be {@code null}. 356 * @param validationResults A list of the results obtained when 357 * validating the password against the 358 * password quality requirements. This must 359 * be {@code null} or empty if the 360 * {@code responseType} element has a value 361 * other than {@code VALIDATION_DETAILS}. 362 * @param missingCurrentPassword Indicates whether the associated operation 363 * is a self change that failed (or would have 364 * failed if not for additional validation 365 * failures) because the user did not provide 366 * his/her current password as required. 367 * @param mustChangePassword Indicates whether the associated operation 368 * is an add or administrative reset that will 369 * require the user to change his/her password 370 * immediately after authenticating before 371 * allowing them to perform any other 372 * operation in the server. 373 * @param secondsUntilExpiration The maximum length of time, in seconds, 374 * that the newly-set password will be 375 * considered valid. This may be {@code null} 376 * if the new password will be considered 377 * valid indefinitely. 378 */ 379 public PasswordValidationDetailsResponseControl( 380 @NotNull final PasswordValidationDetailsResponseType responseType, 381 @Nullable final Collection<PasswordQualityRequirementValidationResult> 382 validationResults, 383 final boolean missingCurrentPassword, 384 final boolean mustChangePassword, 385 @Nullable final Integer secondsUntilExpiration) 386 { 387 super(PASSWORD_VALIDATION_DETAILS_RESPONSE_OID, false, 388 encodeValue(responseType, validationResults, missingCurrentPassword, 389 mustChangePassword, secondsUntilExpiration)); 390 391 this.responseType = responseType; 392 this.missingCurrentPassword = missingCurrentPassword; 393 this.mustChangePassword = mustChangePassword; 394 this.secondsUntilExpiration = secondsUntilExpiration; 395 396 if (validationResults == null) 397 { 398 this.validationResults = Collections.emptyList(); 399 } 400 else 401 { 402 this.validationResults = Collections.unmodifiableList( 403 new ArrayList<>(validationResults)); 404 } 405 } 406 407 408 409 /** 410 * Creates a new password validation details response control by decoding the 411 * provided generic control information. 412 * 413 * @param oid The OID for the control. 414 * @param isCritical Indicates whether the control should be considered 415 * critical. 416 * @param value The value for the control. 417 * 418 * @throws LDAPException If the provided information cannot be decoded to 419 * create a password validation details response 420 * control. 421 */ 422 public PasswordValidationDetailsResponseControl(@NotNull final String oid, 423 final boolean isCritical, 424 @Nullable final ASN1OctetString value) 425 throws LDAPException 426 { 427 super(oid, isCritical, value); 428 429 if (value == null) 430 { 431 throw new LDAPException(ResultCode.DECODING_ERROR, 432 ERR_PW_VALIDATION_RESPONSE_NO_VALUE.get()); 433 } 434 435 try 436 { 437 final ASN1Element[] elements = 438 ASN1Sequence.decodeAsSequence(value.getValue()).elements(); 439 440 responseType = PasswordValidationDetailsResponseType.forBERType( 441 elements[0].getType()); 442 if (responseType == null) 443 { 444 throw new LDAPException(ResultCode.DECODING_ERROR, 445 ERR_PW_VALIDATION_RESPONSE_INVALID_RESPONSE_TYPE.get( 446 StaticUtils.toHex(elements[0].getType()))); 447 } 448 449 if (responseType == 450 PasswordValidationDetailsResponseType.VALIDATION_DETAILS) 451 { 452 final ASN1Element[] resultElements = 453 ASN1Sequence.decodeAsSequence(elements[0]).elements(); 454 455 final ArrayList<PasswordQualityRequirementValidationResult> resultList = 456 new ArrayList<>(resultElements.length); 457 for (final ASN1Element e : resultElements) 458 { 459 resultList.add(PasswordQualityRequirementValidationResult.decode(e)); 460 } 461 validationResults = Collections.unmodifiableList(resultList); 462 } 463 else 464 { 465 validationResults = Collections.emptyList(); 466 } 467 468 boolean missingCurrent = false; 469 boolean mustChange = false; 470 Integer secondsRemaining = null; 471 for (int i=1; i < elements.length; i++) 472 { 473 switch (elements[i].getType()) 474 { 475 case TYPE_MISSING_CURRENT_PASSWORD: 476 missingCurrent = 477 ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue(); 478 break; 479 480 case TYPE_MUST_CHANGE_PW: 481 mustChange = 482 ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue(); 483 break; 484 485 case TYPE_SECONDS_UNTIL_EXPIRATION: 486 secondsRemaining = 487 ASN1Integer.decodeAsInteger(elements[i]).intValue(); 488 break; 489 490 default: 491 // We may update this control in the future to provide support for 492 // returning additional password-related information. If we 493 // encounter an unrecognized element, just ignore it rather than 494 // throwing an exception. 495 break; 496 } 497 } 498 499 missingCurrentPassword = missingCurrent; 500 mustChangePassword = mustChange; 501 secondsUntilExpiration = secondsRemaining; 502 } 503 catch (final LDAPException le) 504 { 505 Debug.debugException(le); 506 throw le; 507 } 508 catch (final Exception e) 509 { 510 Debug.debugException(e); 511 throw new LDAPException(ResultCode.DECODING_ERROR, 512 ERR_PW_VALIDATION_RESPONSE_ERROR_PARSING_VALUE.get( 513 StaticUtils.getExceptionMessage(e)), 514 e); 515 } 516 } 517 518 519 520 /** 521 * Encodes the provided information to an ASN.1 element suitable for use as 522 * the control value. 523 * 524 * @param responseType The response type for this password 525 * validation details response control. This 526 * must not be {@code null}. 527 * @param validationResults A list of the results obtained when 528 * validating the password against the 529 * password quality requirements. This must 530 * be {@code null} or empty if the 531 * {@code responseType} element has a value 532 * other than {@code VALIDATION_DETAILS}. 533 * @param missingCurrentPassword Indicates whether the associated operation 534 * is a self change that failed (or would have 535 * failed if not for additional validation 536 * failures) because the user did not provide 537 * his/her current password as required. 538 * @param mustChangePassword Indicates whether the associated operation 539 * is an add or administrative reset that will 540 * require the user to change his/her password 541 * immediately after authenticating before 542 * allowing them to perform any other 543 * operation in the server. 544 * @param secondsUntilExpiration The maximum length of time, in seconds, 545 * that the newly-set password will be 546 * considered valid. This may be {@code null} 547 * if the new password will be considered 548 * valid indefinitely. 549 * 550 * @return The encoded control value. 551 */ 552 @NotNull() 553 private static ASN1OctetString encodeValue( 554 @NotNull final PasswordValidationDetailsResponseType responseType, 555 @Nullable final Collection<PasswordQualityRequirementValidationResult> 556 validationResults, 557 final boolean missingCurrentPassword, 558 final boolean mustChangePassword, 559 @Nullable final Integer secondsUntilExpiration) 560 { 561 final ArrayList<ASN1Element> elements = new ArrayList<>(4); 562 563 switch (responseType) 564 { 565 case VALIDATION_DETAILS: 566 if (validationResults == null) 567 { 568 elements.add(new ASN1Sequence(responseType.getBERType())); 569 } 570 else 571 { 572 final ArrayList<ASN1Element> resultElements = 573 new ArrayList<>(validationResults.size()); 574 for (final PasswordQualityRequirementValidationResult r : 575 validationResults) 576 { 577 resultElements.add(r.encode()); 578 } 579 elements.add(new ASN1Sequence(responseType.getBERType(), 580 resultElements)); 581 } 582 break; 583 584 case NO_PASSWORD_PROVIDED: 585 case MULTIPLE_PASSWORDS_PROVIDED: 586 case NO_VALIDATION_ATTEMPTED: 587 elements.add(new ASN1Null(responseType.getBERType())); 588 break; 589 } 590 591 if (missingCurrentPassword) 592 { 593 elements.add(new ASN1Boolean(TYPE_MISSING_CURRENT_PASSWORD, 594 missingCurrentPassword)); 595 } 596 597 if (mustChangePassword) 598 { 599 elements.add(new ASN1Boolean(TYPE_MUST_CHANGE_PW, mustChangePassword)); 600 } 601 602 if (secondsUntilExpiration != null) 603 { 604 elements.add(new ASN1Integer(TYPE_SECONDS_UNTIL_EXPIRATION, 605 secondsUntilExpiration)); 606 } 607 608 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 609 } 610 611 612 613 /** 614 * Retrieves the response type for this password validation details response 615 * control. 616 * 617 * @return The response type for this password validation details response 618 * control. 619 */ 620 @NotNull() 621 public PasswordValidationDetailsResponseType getResponseType() 622 { 623 return responseType; 624 } 625 626 627 628 /** 629 * Retrieves a list of the results obtained when attempting to validate the 630 * proposed password against the password quality requirements in effect for 631 * the operation. 632 * 633 * @return A list of the results obtained when attempting to validate the 634 * proposed password against the password quality requirements in 635 * effect for the operation, or an empty list if no validation 636 * results are available. 637 */ 638 @NotNull() 639 public List<PasswordQualityRequirementValidationResult> getValidationResults() 640 { 641 return validationResults; 642 } 643 644 645 646 /** 647 * Indicates whether the associated operation is a self password change that 648 * requires the user to provide his/her current password when setting a new 649 * password, but no current password was provided. 650 * 651 * @return {@code true} if the associated operation is a self password change 652 * that requires the user to provide his/her current password when 653 * setting a new password but none was required, or {@code false} if 654 * the associated operation was not a self change, or if the user's 655 * current password was provided. 656 */ 657 public boolean missingCurrentPassword() 658 { 659 return missingCurrentPassword; 660 } 661 662 663 664 /** 665 * Indicates whether the user will be required to immediately change his/her 666 * password after the associated add or administrative reset is complete. 667 * 668 * @return {@code true} if the associated operation is an add or 669 * administrative reset and the user will be required to change 670 * his/her password before being allowed to perform any other 671 * operation, or {@code false} if the associated operation was not am 672 * add or an administrative reset, or if the user will not be 673 * required to immediately change his/her password. 674 */ 675 public boolean mustChangePassword() 676 { 677 return mustChangePassword; 678 } 679 680 681 682 /** 683 * Retrieves the maximum length of time, in seconds, that the newly-set 684 * password will be considered valid. If {@link #mustChangePassword()} 685 * returns {@code true}, then this value will be the length of time that the 686 * user has to perform a self password change before the account becomes 687 * locked. If {@code mustChangePassword()} returns {@code false}, then this 688 * value will be the length of time until the password expires. 689 * 690 * @return The maximum length of time, in seconds, that the newly-set 691 * password will be considered valid, or {@code null} if the new 692 * password will be valid indefinitely. 693 */ 694 @Nullable() 695 public Integer getSecondsUntilExpiration() 696 { 697 return secondsUntilExpiration; 698 } 699 700 701 702 /** 703 * {@inheritDoc} 704 */ 705 @Override() 706 @NotNull() 707 public PasswordValidationDetailsResponseControl decodeControl( 708 @NotNull final String oid, final boolean isCritical, 709 @Nullable final ASN1OctetString value) 710 throws LDAPException 711 { 712 return new PasswordValidationDetailsResponseControl(oid, isCritical, value); 713 } 714 715 716 717 /** 718 * Extracts a password validation details response control from the provided 719 * result. 720 * 721 * @param result The result from which to retrieve the password validation 722 * details response control. 723 * 724 * @return The password validation details response control contained in the 725 * provided result, or {@code null} if the result did not contain a 726 * password validation details response control. 727 * 728 * @throws LDAPException If a problem is encountered while attempting to 729 * decode the password validation details response 730 * control contained in the provided result. 731 */ 732 @Nullable() 733 public static PasswordValidationDetailsResponseControl get( 734 @NotNull final LDAPResult result) 735 throws LDAPException 736 { 737 final Control c = 738 result.getResponseControl(PASSWORD_VALIDATION_DETAILS_RESPONSE_OID); 739 if (c == null) 740 { 741 return null; 742 } 743 744 if (c instanceof PasswordValidationDetailsResponseControl) 745 { 746 return (PasswordValidationDetailsResponseControl) c; 747 } 748 else 749 { 750 return new PasswordValidationDetailsResponseControl(c.getOID(), 751 c.isCritical(), c.getValue()); 752 } 753 } 754 755 756 757 /** 758 * Extracts a password validation details response control from the provided 759 * result. 760 * 761 * @param exception The exception that was thrown when trying to process the 762 * associated operation. 763 * 764 * @return The password validation details response control contained in the 765 * provided result, or {@code null} if the result did not contain a 766 * password validation details response control. 767 * 768 * @throws LDAPException If a problem is encountered while attempting to 769 * decode the password validation details response 770 * control contained in the provided result. 771 */ 772 @NotNull() 773 public static PasswordValidationDetailsResponseControl get( 774 @NotNull final LDAPException exception) 775 throws LDAPException 776 { 777 return get(exception.toLDAPResult()); 778 } 779 780 781 782 /** 783 * {@inheritDoc} 784 */ 785 @Override() 786 @NotNull() 787 public String getControlName() 788 { 789 return INFO_CONTROL_NAME_PW_VALIDATION_RESPONSE.get(); 790 } 791 792 793 794 /** 795 * Retrieves a representation of this password validation details response 796 * control as a JSON object. The JSON object uses the following fields: 797 * <UL> 798 * <LI> 799 * {@code oid} -- A mandatory string field whose value is the object 800 * identifier for this control. For the password validation details 801 * response control, the OID is "1.3.6.1.4.1.30221.2.5.41". 802 * </LI> 803 * <LI> 804 * {@code control-name} -- An optional string field whose value is a 805 * human-readable name for this control. This field is only intended for 806 * descriptive purposes, and when decoding a control, the {@code oid} 807 * field should be used to identify the type of control. 808 * </LI> 809 * <LI> 810 * {@code criticality} -- A mandatory Boolean field used to indicate 811 * whether this control is considered critical. 812 * </LI> 813 * <LI> 814 * {@code value-base64} -- An optional string field whose value is a 815 * base64-encoded representation of the raw value for this password 816 * validation details response control. Exactly one of the 817 * {@code value-base64} and {@code value-json} fields must be present. 818 * </LI> 819 * <LI> 820 * {@code value-json} -- An optional JSON object field whose value is a 821 * user-friendly representation of the value for this password validation 822 * details response control. Exactly one of the {@code value-base64} and 823 * {@code value-json} fields must be present, and if the 824 * {@code value-json} field is used, then it will use the following 825 * fields: 826 * <UL> 827 * <LI> 828 * {@code response-type} -- A string field that specifies the result 829 * of the password validation processing for the attempt. The value 830 * will be one of "{@code validation-performed}", 831 * "{@code no-password-provided}", 832 * "{@code multiple-passwords-provided}", or 833 * "{@code no-validation-attempted}". 834 * </LI> 835 * <LI> 836 * {@code validation-details} -- An optional array field whose values 837 * are JSON objects with information about the types of validation 838 * performed for the new password. The fields that may be used in 839 * these JSON objects include: 840 * <UL> 841 * <LI> 842 * {@code password-quality-requirement} -- A JSON object whose 843 * value provides information about a password quality requirement 844 * that was evaluated. The fields used in these JSON objects 845 * include: 846 * <UL> 847 * <LI> 848 * {@code description} -- A string field whose value is a 849 * user-friendly description of the password quality 850 * requirement. 851 * </LI> 852 * <LI> 853 * {@code client-side-validation-type} -- An optional string 854 * field whose value is an identifier that the client can use 855 * to programmatically determine the type of requirement. 856 * </LI> 857 * <LI> 858 * {@code client-side-validation-properties} -- An optional 859 * array field whose values are JSON objects with additional 860 * properties that the client can use in the course of 861 * programmatically determining whether a proposed password is 862 * likely to satisfy the requirement. Each of these JSON 863 * objects will include a {@code name} field whose value is a 864 * string that specifies the property name, and a 865 * {@code value} field whose value is a string that specifies 866 * the property value. 867 * </LI> 868 * </UL> 869 * </LI> 870 * <LI> 871 * {@code requirement-satisfied} -- A Boolean field that indicates 872 * whether the provided new password satisfies the password 873 * quality requirement. 874 * </LI> 875 * <LI> 876 * {@code additional-information} -- An optional string field 877 * whose value provides additional information about the 878 * validation for the associated requirement. 879 * </LI> 880 * </UL> 881 * </LI> 882 * <LI> 883 * {@code missing-current-password} -- A Boolean field that indicates 884 * whether the server requires the user's current password to be 885 * provided when choosing a new password, but that password was not 886 * provided. 887 * </LI> 888 * <LI> 889 * {@code must-change-password} -- A Boolean field that indicates 890 * whether the user will be required to choose a new password before 891 * they will be allowed to request any other operations. 892 * </LI> 893 * <LI> 894 * {@code seconds-until-expiration} -- An optional integer field whose 895 * value is the number of seconds until the new password will 896 * expire. 897 * </LI> 898 * </UL> 899 * </LI> 900 * </UL> 901 * 902 * @return A JSON object that contains a representation of this control. 903 */ 904 @Override() 905 @NotNull() 906 public JSONObject toJSONControl() 907 { 908 final Map<String,JSONValue> valueFields = new LinkedHashMap<>(); 909 910 switch (responseType) 911 { 912 case VALIDATION_DETAILS: 913 valueFields.put(JSON_FIELD_RESPONSE_TYPE, 914 new JSONString(JSON_RESPONSE_TYPE_VALIDATION_PERFORMED)); 915 916 final List<JSONValue> validationDetailsValues = 917 new ArrayList<>(validationResults.size()); 918 for (final PasswordQualityRequirementValidationResult result : 919 validationResults) 920 { 921 validationDetailsValues.add(encodeValidationResultJSON(result)); 922 } 923 valueFields.put(JSON_FIELD_VALIDATION_DETAILS, 924 new JSONArray(validationDetailsValues)); 925 break; 926 927 case NO_PASSWORD_PROVIDED: 928 valueFields.put(JSON_FIELD_RESPONSE_TYPE, 929 new JSONString(JSON_RESPONSE_TYPE_NO_PASSWORD_PROVIDED)); 930 break; 931 932 case MULTIPLE_PASSWORDS_PROVIDED: 933 valueFields.put(JSON_FIELD_RESPONSE_TYPE, 934 new JSONString(JSON_RESPONSE_TYPE_MULTIPLE_PASSWORDS_PROVIDED)); 935 break; 936 937 case NO_VALIDATION_ATTEMPTED: 938 valueFields.put(JSON_FIELD_RESPONSE_TYPE, 939 new JSONString(JSON_RESPONSE_TYPE_NO_VALIDATION_ATTEMPTED)); 940 break; 941 } 942 943 valueFields.put(JSON_FIELD_MISSING_CURRENT_PASSWORD, 944 new JSONBoolean(missingCurrentPassword)); 945 946 valueFields.put(JSON_FIELD_MUST_CHANGE_PASSWORD, 947 new JSONBoolean(mustChangePassword)); 948 949 if (secondsUntilExpiration != null) 950 { 951 valueFields.put(JSON_FIELD_SECONDS_UNTIL_EXPIRATION, 952 new JSONNumber(secondsUntilExpiration)); 953 } 954 955 return new JSONObject( 956 new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID, 957 PASSWORD_VALIDATION_DETAILS_RESPONSE_OID), 958 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME, 959 INFO_CONTROL_NAME_PW_VALIDATION_RESPONSE.get()), 960 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY, 961 isCritical()), 962 new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON, 963 new JSONObject(valueFields))); 964 } 965 966 967 968 /** 969 * Encodes the provided password quality requirement validation result to a 970 * JSON object. 971 * 972 * @param result The result to be encoded. It must not be {@code null}. 973 * 974 * @return A JSON object containing the encoded result. 975 */ 976 @NotNull() 977 private static JSONObject encodeValidationResultJSON( 978 @NotNull final PasswordQualityRequirementValidationResult result) 979 { 980 final PasswordQualityRequirement requirement = 981 result.getPasswordRequirement(); 982 final Map<String,JSONValue> requirementFields = new LinkedHashMap<>(); 983 984 requirementFields.put(JSON_FIELD_DESCRIPTION, 985 new JSONString(requirement.getDescription())); 986 987 final String clientSideValidationType = 988 requirement.getClientSideValidationType(); 989 if (clientSideValidationType != null) 990 { 991 requirementFields.put(JSON_FIELD_CLIENT_SIDE_VALIDATION_TYPE, 992 new JSONString(clientSideValidationType)); 993 } 994 995 final Map<String,String> clientSideValidationProperties = 996 requirement.getClientSideValidationProperties(); 997 if (! clientSideValidationProperties.isEmpty()) 998 { 999 final List<JSONValue> propertyValues = 1000 new ArrayList<>(clientSideValidationProperties.size()); 1001 for (final Map.Entry<String,String> e : 1002 clientSideValidationProperties.entrySet()) 1003 { 1004 propertyValues.add(new JSONObject( 1005 new JSONField(JSON_FIELD_PROPERTY_NAME, e.getKey()), 1006 new JSONField(JSON_FIELD_PROPERTY_VALUE, e.getValue()))); 1007 } 1008 1009 requirementFields.put(JSON_FIELD_CLIENT_SIDE_VALIDATION_PROPERTIES, 1010 new JSONArray(propertyValues)); 1011 } 1012 1013 1014 final Map<String,JSONValue> detailsFields = new LinkedHashMap<>(); 1015 1016 detailsFields.put(JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT, 1017 new JSONObject(requirementFields)); 1018 1019 detailsFields.put(JSON_FIELD_REQUIREMENT_SATISFIED, 1020 new JSONBoolean(result.requirementSatisfied())); 1021 1022 final String additionalInformation = result.getAdditionalInfo(); 1023 if (additionalInformation != null) 1024 { 1025 detailsFields.put(JSON_FIELD_ADDITIONAL_INFORMATION, 1026 new JSONString(additionalInformation)); 1027 } 1028 1029 return new JSONObject(detailsFields); 1030 } 1031 1032 1033 1034 /** 1035 * Attempts to decode the provided object as a JSON representation of a 1036 * password validation details response control. 1037 * 1038 * @param controlObject The JSON object to be decoded. It must not be 1039 * {@code null}. 1040 * @param strict Indicates whether to use strict mode when decoding 1041 * the provided JSON object. If this is {@code true}, 1042 * then this method will throw an exception if the 1043 * provided JSON object contains any unrecognized 1044 * fields. If this is {@code false}, then unrecognized 1045 * fields will be ignored. 1046 * 1047 * @return The password validation details response control that was decoded 1048 * from the provided JSON object. 1049 * 1050 * @throws LDAPException If the provided JSON object cannot be parsed as a 1051 * valid password validation details response control. 1052 */ 1053 @NotNull() 1054 public static PasswordValidationDetailsResponseControl decodeJSONControl( 1055 @NotNull final JSONObject controlObject, 1056 final boolean strict) 1057 throws LDAPException 1058 { 1059 final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper( 1060 controlObject, strict, true, true); 1061 1062 final ASN1OctetString rawValue = jsonControl.getRawValue(); 1063 if (rawValue != null) 1064 { 1065 return new PasswordValidationDetailsResponseControl(jsonControl.getOID(), 1066 jsonControl.getCriticality(), rawValue); 1067 } 1068 1069 1070 final JSONObject valueObject = jsonControl.getValueObject(); 1071 1072 final String responseTypeStr = 1073 valueObject.getFieldAsString(JSON_FIELD_RESPONSE_TYPE); 1074 if (responseTypeStr == null) 1075 { 1076 throw new LDAPException(ResultCode.DECODING_ERROR, 1077 ERR_PW_VALIDATION_RESPONSE_JSON_VALUE_MISSING_FIELD.get( 1078 controlObject.toSingleLineString(), JSON_FIELD_RESPONSE_TYPE)); 1079 } 1080 1081 final PasswordValidationDetailsResponseType responseType; 1082 switch (responseTypeStr) 1083 { 1084 case JSON_RESPONSE_TYPE_VALIDATION_PERFORMED: 1085 responseType = PasswordValidationDetailsResponseType.VALIDATION_DETAILS; 1086 break; 1087 case JSON_RESPONSE_TYPE_NO_PASSWORD_PROVIDED: 1088 responseType = 1089 PasswordValidationDetailsResponseType.NO_PASSWORD_PROVIDED; 1090 break; 1091 case JSON_RESPONSE_TYPE_MULTIPLE_PASSWORDS_PROVIDED: 1092 responseType = 1093 PasswordValidationDetailsResponseType.MULTIPLE_PASSWORDS_PROVIDED; 1094 break; 1095 case JSON_RESPONSE_TYPE_NO_VALIDATION_ATTEMPTED: 1096 responseType = 1097 PasswordValidationDetailsResponseType.NO_VALIDATION_ATTEMPTED; 1098 break; 1099 default: 1100 throw new LDAPException(ResultCode.DECODING_ERROR, 1101 ERR_PW_VALIDATION_RESPONSE_JSON_UNKNOWN_RESPONSE_TYPE.get( 1102 controlObject.toSingleLineString(), JSON_FIELD_RESPONSE_TYPE, 1103 responseTypeStr)); 1104 } 1105 1106 1107 final List<PasswordQualityRequirementValidationResult> validationResults; 1108 final List<JSONValue> validationDetailsValues = 1109 valueObject.getFieldAsArray(JSON_FIELD_VALIDATION_DETAILS); 1110 if (validationDetailsValues == null) 1111 { 1112 validationResults = Collections.emptyList(); 1113 } 1114 else 1115 { 1116 validationResults = new ArrayList<>(validationDetailsValues.size()); 1117 for (final JSONValue v : validationDetailsValues) 1118 { 1119 validationResults.add(decodeValidationResultJSON(controlObject, v, 1120 strict)); 1121 } 1122 } 1123 1124 1125 final Boolean missingCurrentPassword = 1126 valueObject.getFieldAsBoolean(JSON_FIELD_MISSING_CURRENT_PASSWORD); 1127 if (missingCurrentPassword == null) 1128 { 1129 throw new LDAPException(ResultCode.DECODING_ERROR, 1130 ERR_PW_VALIDATION_RESPONSE_JSON_VALUE_MISSING_FIELD.get( 1131 controlObject.toSingleLineString(), 1132 JSON_FIELD_MISSING_CURRENT_PASSWORD)); 1133 } 1134 1135 1136 final Boolean mustChangePassword = 1137 valueObject.getFieldAsBoolean(JSON_FIELD_MUST_CHANGE_PASSWORD); 1138 if (mustChangePassword == null) 1139 { 1140 throw new LDAPException(ResultCode.DECODING_ERROR, 1141 ERR_PW_VALIDATION_RESPONSE_JSON_VALUE_MISSING_FIELD.get( 1142 controlObject.toSingleLineString(), 1143 JSON_FIELD_MUST_CHANGE_PASSWORD)); 1144 } 1145 1146 1147 final Integer secondsUntilExpiration = 1148 valueObject.getFieldAsInteger(JSON_FIELD_SECONDS_UNTIL_EXPIRATION); 1149 1150 1151 if (strict) 1152 { 1153 final List<String> unrecognizedFields = 1154 JSONControlDecodeHelper.getControlObjectUnexpectedFields( 1155 valueObject, JSON_FIELD_RESPONSE_TYPE, 1156 JSON_FIELD_VALIDATION_DETAILS, 1157 JSON_FIELD_MISSING_CURRENT_PASSWORD, 1158 JSON_FIELD_MUST_CHANGE_PASSWORD, 1159 JSON_FIELD_SECONDS_UNTIL_EXPIRATION); 1160 if (! unrecognizedFields.isEmpty()) 1161 { 1162 throw new LDAPException(ResultCode.DECODING_ERROR, 1163 ERR_PW_VALIDATION_RESPONSE_JSON_UNRECOGNIZED_FIELD.get( 1164 controlObject.toSingleLineString(), 1165 unrecognizedFields.get(0))); 1166 } 1167 } 1168 1169 1170 return new PasswordValidationDetailsResponseControl(responseType, 1171 validationResults, missingCurrentPassword, mustChangePassword, 1172 secondsUntilExpiration); 1173 } 1174 1175 1176 1177 /** 1178 * Decodes the provided JSON value as a password quality requirement 1179 * validation result. 1180 * 1181 * @param controlObject A JSON object containing an encoded representation 1182 * of the control being decoded. It must not be 1183 * {@code null}. 1184 * @param resultValue The JSON value to be decoded as a password quality 1185 * requirement validation result. It must not be 1186 * {@code null}. 1187 * @param strict Indicates whether to use strict mode when decoding 1188 * the provided JSON object. If this is {@code true}, 1189 * then this method will throw an exception if the 1190 * provided JSON value is an object that contains any 1191 * unrecognized fields. If this is {@code false}, then 1192 * unrecognized fields will be ignored. 1193 * 1194 * @return The password quality requirement validation result that was 1195 * decoded. 1196 * 1197 * @throws LDAPException If the provided JSON value cannot be decoded as a 1198 * password quality requirement validation result. 1199 */ 1200 @NotNull() 1201 private static PasswordQualityRequirementValidationResult 1202 decodeValidationResultJSON( 1203 @NotNull final JSONObject controlObject, 1204 @NotNull final JSONValue resultValue, 1205 final boolean strict) 1206 throws LDAPException 1207 { 1208 if (! (resultValue instanceof JSONObject)) 1209 { 1210 throw new LDAPException(ResultCode.DECODING_ERROR, 1211 ERR_PW_VALIDATION_RESPONSE_JSON_RESULT_NOT_OBJECT.get( 1212 controlObject.toSingleLineString(), 1213 JSON_FIELD_VALIDATION_DETAILS)); 1214 } 1215 1216 final JSONObject resultObject = (JSONObject) resultValue; 1217 1218 final JSONObject requirementObject = 1219 resultObject.getFieldAsObject(JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT); 1220 if (requirementObject == null) 1221 { 1222 throw new LDAPException(ResultCode.DECODING_ERROR, 1223 ERR_PW_VALIDATION_RESPONSE_JSON_RESULT_MISSING_FIELD.get( 1224 controlObject.toSingleLineString(), 1225 JSON_FIELD_VALIDATION_DETAILS, 1226 JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT)); 1227 } 1228 1229 final String requirementDescription = 1230 requirementObject.getFieldAsString(JSON_FIELD_DESCRIPTION); 1231 if (requirementDescription == null) 1232 { 1233 throw new LDAPException(ResultCode.DECODING_ERROR, 1234 ERR_PW_VALIDATION_RESPONSE_JSON_REQUIREMENT_MISSING_FIELD.get( 1235 controlObject.toSingleLineString(), 1236 JSON_FIELD_VALIDATION_DETAILS, 1237 JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT, 1238 JSON_FIELD_DESCRIPTION)); 1239 } 1240 1241 final String clientSideValidationType = requirementObject.getFieldAsString( 1242 JSON_FIELD_CLIENT_SIDE_VALIDATION_TYPE); 1243 1244 final Map<String,String> clientSideValidationProperties = 1245 new LinkedHashMap<>(); 1246 final List<JSONValue> clientSideValidationPropertyValues = 1247 requirementObject.getFieldAsArray( 1248 JSON_FIELD_CLIENT_SIDE_VALIDATION_PROPERTIES); 1249 if (clientSideValidationPropertyValues != null) 1250 { 1251 for (final JSONValue v : clientSideValidationPropertyValues) 1252 { 1253 if (! (v instanceof JSONObject)) 1254 { 1255 throw new LDAPException(ResultCode.DECODING_ERROR, 1256 ERR_PW_VALIDATION_RESPONSE_JSON_REQUIREMENT_PROP_NOT_OBJECT.get( 1257 controlObject.toSingleLineString(), 1258 JSON_FIELD_VALIDATION_DETAILS, 1259 JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT, 1260 JSON_FIELD_CLIENT_SIDE_VALIDATION_PROPERTIES)); 1261 } 1262 1263 final JSONObject propObject = (JSONObject) v; 1264 final String name = 1265 propObject.getFieldAsString(JSON_FIELD_PROPERTY_NAME); 1266 if (name == null) 1267 { 1268 throw new LDAPException(ResultCode.DECODING_ERROR, 1269 ERR_PW_VALIDATION_RESPONSE_JSON_REQUIREMENT_PROP_MISSING_FIELD. 1270 get(controlObject.toSingleLineString(), 1271 JSON_FIELD_VALIDATION_DETAILS, 1272 JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT, 1273 JSON_FIELD_CLIENT_SIDE_VALIDATION_PROPERTIES, 1274 JSON_FIELD_PROPERTY_NAME)); 1275 } 1276 1277 final String value = 1278 propObject.getFieldAsString(JSON_FIELD_PROPERTY_VALUE); 1279 if (value == null) 1280 { 1281 throw new LDAPException(ResultCode.DECODING_ERROR, 1282 ERR_PW_VALIDATION_RESPONSE_JSON_REQUIREMENT_PROP_MISSING_FIELD. 1283 get(controlObject.toSingleLineString(), 1284 JSON_FIELD_VALIDATION_DETAILS, 1285 JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT, 1286 JSON_FIELD_CLIENT_SIDE_VALIDATION_PROPERTIES, 1287 JSON_FIELD_PROPERTY_VALUE)); 1288 } 1289 1290 if (strict) 1291 { 1292 final List<String> unrecognizedFields = 1293 JSONControlDecodeHelper.getControlObjectUnexpectedFields( 1294 propObject, JSON_FIELD_PROPERTY_NAME, 1295 JSON_FIELD_PROPERTY_VALUE); 1296 if (! unrecognizedFields.isEmpty()) 1297 { 1298 throw new LDAPException(ResultCode.DECODING_ERROR, 1299 ERR_PW_VALIDATION_RESPONSE_JSON_UNRECOGNIZED_PROP_FIELD.get( 1300 controlObject.toSingleLineString(), 1301 JSON_FIELD_VALIDATION_DETAILS, 1302 JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT, 1303 JSON_FIELD_CLIENT_SIDE_VALIDATION_PROPERTIES, 1304 unrecognizedFields.get(0))); 1305 } 1306 } 1307 1308 clientSideValidationProperties.put(name, value); 1309 } 1310 } 1311 1312 1313 final Boolean requirementSatisfied = 1314 resultObject.getFieldAsBoolean(JSON_FIELD_REQUIREMENT_SATISFIED); 1315 if (requirementSatisfied == null) 1316 { 1317 throw new LDAPException(ResultCode.DECODING_ERROR, 1318 ERR_PW_VALIDATION_RESPONSE_JSON_RESULT_MISSING_FIELD.get( 1319 controlObject.toSingleLineString(), 1320 JSON_FIELD_VALIDATION_DETAILS, 1321 JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT, 1322 JSON_FIELD_REQUIREMENT_SATISFIED)); 1323 } 1324 1325 1326 if (strict) 1327 { 1328 final List<String> unrecognizedFields = 1329 JSONControlDecodeHelper.getControlObjectUnexpectedFields( 1330 resultObject, JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT, 1331 JSON_FIELD_REQUIREMENT_SATISFIED, 1332 JSON_FIELD_ADDITIONAL_INFORMATION); 1333 if (! unrecognizedFields.isEmpty()) 1334 { 1335 throw new LDAPException(ResultCode.DECODING_ERROR, 1336 ERR_PW_VALIDATION_RESPONSE_JSON_RESULT_UNRECOGNIZED_FIELD.get( 1337 controlObject.toSingleLineString(), 1338 JSON_FIELD_VALIDATION_DETAILS, 1339 JSON_FIELD_PASSWORD_QUALITY_REQUIREMENT, 1340 unrecognizedFields.get(0))); 1341 } 1342 } 1343 1344 1345 final String additionalInformation = 1346 resultObject.getFieldAsString(JSON_FIELD_ADDITIONAL_INFORMATION); 1347 1348 final PasswordQualityRequirement requirement = 1349 new PasswordQualityRequirement(requirementDescription, 1350 clientSideValidationType, clientSideValidationProperties); 1351 return new PasswordQualityRequirementValidationResult(requirement, 1352 requirementSatisfied, additionalInformation); 1353 } 1354 1355 1356 1357 /** 1358 * {@inheritDoc} 1359 */ 1360 @Override() 1361 public void toString(@NotNull final StringBuilder buffer) 1362 { 1363 buffer.append("PasswordValidationDetailsResponseControl(responseType='"); 1364 buffer.append(responseType.name()); 1365 buffer.append('\''); 1366 1367 if (responseType == 1368 PasswordValidationDetailsResponseType.VALIDATION_DETAILS) 1369 { 1370 buffer.append(", validationDetails={"); 1371 1372 final Iterator<PasswordQualityRequirementValidationResult> iterator = 1373 validationResults.iterator(); 1374 while (iterator.hasNext()) 1375 { 1376 iterator.next().toString(buffer); 1377 if (iterator.hasNext()) 1378 { 1379 buffer.append(','); 1380 } 1381 } 1382 1383 buffer.append('}'); 1384 } 1385 1386 buffer.append(", missingCurrentPassword="); 1387 buffer.append(missingCurrentPassword); 1388 buffer.append(", mustChangePassword="); 1389 buffer.append(mustChangePassword); 1390 1391 if (secondsUntilExpiration != null) 1392 { 1393 buffer.append(", secondsUntilExpiration="); 1394 buffer.append(secondsUntilExpiration); 1395 } 1396 1397 buffer.append("})"); 1398 } 1399}