Passwords in DS/OpenDJ

This book provides information on passwords in DS/OpenDJ and includes a chapter on the Password Synchronization Plugin.


How does DS/OpenDJ (All versions) store password values?

The purpose of this article is to provide information on the password storage scheme formats used in DS/OpenDJ. This information can help you understand the secureness of different schemes and how they compare. Additionally this information may be helpful if you have passwords hashed in another system and want to convert them for import into DS/OpenDJ.

Password storage scheme formats

Note

See Configuration Reference › Password Storage Scheme for details of what properties are allowed for each scheme.

The following table provides details of the password scheme formats in use in DS/OpenDJ, where:

  • <digest> is the bytes returned from the relevant asymmetric hashing function.
  • <encrypted> is the bytes returned from the relevant symmetric encryption function.
  • <salt> is the salt bytes.
  • base64(...) is the standard base64 encoding of ...
  • "" surrounds literal characters in the password value.

Password Schemes 

Name Available from DS/OpenDJ Salt size (bytes) Key size (bits) Format
AES 2.4   128 "{AES}" base64(<encrypted>)
Base64 2.4     "{BASE64}" base64(<password>)
BCrypt * 3.5 16   "{BCRYPT}" <digest>
Blowfish 2.4   128 "{BLOWFISH}" base64(<encrypted>)
Clear 2.4     "{CLEAR}" <password>
Crypt (UNIX) ** 2.4 2   "{CRYPT}" <digest>
Crypt (MD5) *** 2.4 8   "{CRYPT}" "$1$" <digest>
Crypt (SHA256) *** 2.6 8   "{CRYPT}" "$5$" <digest>
Crypt (SHA2512) *** 2.6 8   "{CRYPT}" "$6$" <digest>
MD5 2.4     "{MD5}" base64(<digest>)
PBKDF2 2.6 8   "{PBKDF2}" <iterations> ":" base64(<digest> <salt>)
PKCS5S2 **** 3.0 16   "{PKCS5S2}" base64(<digest> <salt>)
RC4 2.4   128 "{RC4}" base64(<encrypted>)
SaltedMD5 2.4 8   "{SMD5}" base64(<digest> <salt>)
SaltedSHA1 2.4 8   "{SSHA}" base64(<digest> <salt>)
SaltedSHA256 2.4 8   "{SSHA256}" base64(<digest> <salt>)
SaltedSHA384 2.4 8   "{SSHA384}" base64(<digest> <salt>)
SaltedSHA512 2.4 8   "{SSHA512}" base64(<digest> <salt>)
SHA1 2.4     "{SHA}" base64(<digest>)
TripleDes 2.4   168 "{3DES}" base64(<encrypted>)

* This <digest> is OpenBSD's bcrypt implementation, which uses a custom base64 encoding. See bcrypt - A FutureAdaptable Password Scheme for further information.

** This is a traditional Unix algorithm (12-bit salt).

*** See Modular Crypt Format for further information.

**** See Atlassian’s PBKDF2-based Hash for further information.

See Also

FAQ: Passwords in DS/OpenDJ

Administration Guide › Configuring Password Policy › Configuring Password Storage

Configuration Reference › Password Storage Scheme

Related Training

N/A

Related Issue Tracker IDs

N/A


How does password expiration work in DS/OpenDJ (All versions)?

The purpose of this article is to provide information on how password expiration works in DS/OpenDJ.

Password expiration

If you set up password expiration, the warning gets triggered when the user authenticates during the password expiration warning interval and the ds-pwp-warned-time attribute is set. If the user does not authenticate before the password expiry time, the ds-pwp-password-expiration-time value will keep increasing until the user password is changed and the expiry time is reset.

In the following examples, the password policy will expire a password one hour after it has been changed and warn 30 minutes before expiry time: 

  • The first example demonstrates what happens if the user does not authenticate before their password expires and explains why you might see the password expiry time continually increasing.
  • The second example demonstrates the expected outcome where the user authenticates before their password expires and within the warning period. 

Scenario 1 - User does not authenticate before password expiry

In this example, the user does not authenticate before their password expires; this causes the password expiry time to keep increasing and the user to be locked out:

$ ./ldapsearch --port 1389 --baseDN dc=example,dc=com --bindDN "cn=Directory Manager" --bindPassword password uid=user.6 ds-pwp-password-expiration-time pwdChangedtime ds-pwp-warned-time

dn: uid=user.6,ou=People,dc=example,dc=com
pwdChangedTime: 20170208115546.098Z
ds-pwp-password-expiration-time: 20170208125546.098Z
Note

Notice that for some accounts, the ds-pwp-password-expiration-time attribute keeps changing once the password-expiration-warning-interval has passed, which means the password never expires.

When the expiry time is reached and the user attempts to authenticate, the user is locked out. The password expiry time continues to increase to keep the account locked until the password is reset:

$ ./ldapsearch --port 1389 --baseDN dc=example,dc=com --bindDN "uid=user.6,ou=People,dc=example,dc=com" --bindPassword cangetin uid=user.6 cn

The simple bind attempt failed
Result Code:  49 (Invalid Credentials)
$ ./ldapsearch --port 1389 --baseDN dc=example,dc=com --bindDN "cn=Directory Manager" --bindPassword password uid=user.6 ds-pwp-password-expiration-time pwdChangedtime ds-pwp-warned-time

dn: uid=user.6,ou=People,dc=example,dc=com
pwdChangedTime: 20170208115546.098Z
ds-pwp-password-expiration-time: 20170208133604.100Z

Once the password is changed, the expiry time is reset to one hour:

$ ./ldapsearch --port 1389 --baseDN dc=example,dc=com --bindDN "cn=Directory Manager" --bindPassword password uid=user.6 ds-pwp-password-expiration-time pwdChangedtime ds-pwp-warned-time

dn: uid=user.6,ou=People,dc=example,dc=com
pwdChangedTime: 20170208131848.246Z
ds-pwp-password-expiration-time: 20170208141848.246Z

Scenario 2 - User authenticates before password expires and within warning period of 30 minutes

In this example, the user authenticates before their password expires and within the warning period; this triggers the notification they see warning that their password is due to expire

$ ./ldapsearch --port 1389 --baseDN dc=example,dc=com --bindDN "cn=Directory Manager" --bindPassword password uid=user.6 ds-pwp-password-expiration-time pwdChangedtime ds-pwp-warned-time

dn: uid=user.6,ou=People,dc=example,dc=com
ds-pwp-password-expiration-time: 20170208141848.246Z
pwdChangedtime: 20170208131848.246Z
$ ./ldapsearch --port 1389 --baseDN dc=example,dc=com --bindDN "uid=user.6,ou=People,dc=example,dc=com" --bindPassword cangetin1  uid=user.6 cn

# Your password will expire in 30 minutes, 0 seconds
dn: uid=user.6,ou=People,dc=example,dc=com
cn: Abagail Abadines
$ ./ldapsearch --port 1389 --baseDN dc=example,dc=com --bindDN "cn=Directory Manager" --bindPassword password uid=user.6 ds-pwp-password-expiration-time pwdChangedtime ds-pwp-warned-time

dn: uid=user.6,ou=People,dc=example,dc=com
ds-pwp-password-expiration-time: 20170208142000.923Z
pwdChangedtime: 20170208131848.246Z
ds-pwp-warned-time: 20170208135000.923Z

See Also

FAQ: Passwords in DS/OpenDJ

How does DS/OpenDJ (All versions) store password values?

Administration Guide › Configuring Password Policy

Related Training

N/A

Related Issue Tracker IDs

N/A


Understanding OpenAM 12.x, 13.x and OpenDJ 2.6.x, 3.x account lockout behaviors

The purpose of this article is to provide information to help you understand OpenAM and OpenDJ account lockout behaviors. Account Lockout can be performed in either OpenAM or the LDAP server itself, this includes a working example to lock a user’s account after 3 invalid password attempts and how to define a Password Policy in OpenDJ.

Background Information

The OpenAM Authentication Service can be configured to lock a user’s account after a defined number of log in attempts has failed. Account Lockout is disabled by default, but when configured properly, this feature can be useful in fending off brute force attacks against OpenAM login screens.

If your OpenAM environment includes an LDAP server (such as OpenDJ) as an authentication database, then you have options on how (and where) you can configure Account Lockout settings. This can be performed in either OpenAM (as mentioned above) or in the LDAP server, itself. But the behavior is different based on where this is configured. There are benefits and drawbacks towards configuring Account Lockout in either product and knowing the difference is essential.

Note

Configuring Account Lockout simultaneously in both products can lead to confusing results and should be avoided unless you have a firm understanding of how each product works.  See the scenario at the end of this article for a deeper dive on Account Lockout from an attribute perspective. 

The OpenAM Approach

You can configure Account Lockout in OpenAM either globally or for a particular realm. To access the Account Lockout settings for the global configuration:

  • OpenAM 13.5 and later console: navigate to: Configure > Authentication > Core Attributes > Account Lockout.
  • Pre-OpenAM 13.5 console: navigate to: Configuration > Authentication > Core > Account Lockout.

To access Account Lockout settings for a particular realm:

  • OpenAM 13 and later console: navigate to: Realms > [Realm Name] > Authentication > Settings > Account Lockout.
  • Pre-OpenAM 13 console: navigate to: Access Control > [Realm Name] > Authentication > All Core Settings > Account Lockout.

In either location you will see various parameters for controlling Account Lockout as follows (image for OpenAM 13 and later):

See How do I enable account lockout in AM/OpenAM (All versions)? for further information on the different types of account lockout.

Account Lockout is disabled by default; you need to enable Login Failure Lockout Mode to enable this feature. Once it is enabled, you configure the number of attempts before an account is locked and even if a warning message is displayed to the user before their account is locked.  You can configure how long the account is locked and even the duration between successive lockouts (which can increase if you set the duration multiplier).  You can configure the attributes to use to store the account lockout information in addition to the default attributes configured in the Data Store.

Enabling Account Lockout affects the following Data Store attributes: inetUserStatus and sunAMAuthInvalidAttemptsData. By default, the value of the inetUserStatus attribute is either Active or Inactive, but this can be configured to use another attribute and another attribute value. This can be configured in the User Configuration section of the Data Store configuration as follows:

These attributes are updated in the Data Store configuration for the realm. A benefit of implementing Account Lockout in OpenAM is that you can use any LDAPv3 directory, Active Directory, or even a relational database – but you do need to have a Data Store configured to provide OpenAM with somewhere to write these values. An additional benefit is that OpenAM is already configured with error messages that can be easily displayed when a user’s account is about to be locked or has become locked. Configuring Account Lockout within OpenAM, however, may not provide the level of granularity that you might need and as such, you may need to configure it in the authentication database (such as OpenDJ).

The OpenDJ Approach

OpenDJ can be configured to lock accounts as well. This is defined in a password policy and can be configured globally (the entire OpenDJ instance) or it may be applied to a subentry (a group of users or a specific user). Similar to OpenAM, a user’s account can be locked after a number of invalid authentication attempts have been made. And similar to OpenAM, you have several additional settings that can be configured to control the lockout period, whether warnings should be sent, and even who to notify when the account has been locked.

But while configuring Account Lockout in OpenAM may recognize invalid password attempts in your SSO environment, configuring it in OpenDJ will recognize invalid attempts for any application that is using OpenDJ as an authentication database. This is more of a centralized approach and can recognize attacks from several vectors.

Configuring Account Lockout in OpenDJ affects the following OpenDJ attributes:

  • pwdFailureTime (a multi-valued attribute consisting of the timestamp of each invalid password attempt).
  • pwdAccountLockedTime (a timestamp indicating when the account was locked).

Another benefit of implementing Account Lockout in OpenDJ is the ability to configure Account Lockout for different types of users. This is helpful when you want to have different password policies for users, administrators, or even service accounts. This is accomplished by assigning different password polices directly to those users or indirectly through groups or virtual attributes. A drawback to this approach, however, is that OpenAM doesn’t necessarily recognize the circumstances behind error messages returned from OpenDJ when a user is unable to log in. A scrambled password in OpenDJ, for instance, simply displays as an Authentication failed error message in the OpenAM login screen.

By default, all users in OpenDJ are automatically assigned a generic (rather lenient) password policy that is aptly named: Default Password Policy. The definition of this policy can be seen as follows:

dn: cn=Default Password Policy,cn=Password Policies,cn=config
objectClass: ds-cfg-password-policy
objectClass: top
objectClass: ds-cfg-authentication-policy
ds-cfg-skip-validation-for-administrators: false
ds-cfg-force-change-on-add: false
ds-cfg-state-update-failure-policy: reactive
ds-cfg-password-history-count: 0
ds-cfg-password-history-duration: 0 seconds
ds-cfg-allow-multiple-password-values: false
ds-cfg-lockout-failure-expiration-interval: 0 seconds
ds-cfg-lockout-failure-count: 0
ds-cfg-max-password-reset-age: 0 seconds
ds-cfg-max-password-age: 0 seconds
ds-cfg-idle-lockout-interval: 0 seconds
ds-cfg-java-class: org.opends.server.core.PasswordPolicyFactory
ds-cfg-lockout-duration: 0 seconds
ds-cfg-grace-login-count: 0
ds-cfg-force-change-on-reset: false
ds-cfg-default-password-storage-scheme: cn=Salted SHA-1,cn=Password Storage Schemes,cn=config
ds-cfg-allow-user-password-changes: true
ds-cfg-allow-pre-encoded-passwords: false
ds-cfg-require-secure-password-changes: false
cn: Default Password Policy
ds-cfg-require-secure-authentication: false
ds-cfg-expire-passwords-without-warning: false
ds-cfg-password-change-requires-current-password: false
ds-cfg-password-generator: cn=Random Password Generator,cn=Password Generators,cn=config
ds-cfg-password-expiration-warning-interval: 5 days
ds-cfg-allow-expired-password-changes: false
ds-cfg-password-attribute: userPassword
ds-cfg-min-password-age: 0 seconds

The value of the ds-cfg-lockout-failure-count attribute is 0; which means that user accounts are not locked by default – no matter how many incorrect attempts are made. This is one of the many security settings that you can configure in a password policy and while many of these mimic what is available in OpenAM, others go quite deeper.

You can use the OpenDJ dsconfig command to change the Default Password Policy as follows:

$ ./dsconfig set-password-policy-prop --policy-name "Default Password Policy" --set lockout-failure-count:3 --hostname localhost --port 4444 --trustAll --bindDN "cn=Directory Manager" --bindPassword ****** --no-prompt

Rather than modifying the Default Password Policy, a preferred method is to create a new password policy and apply your own specific settings to the new policy. This policy can then be applied to a specific set of users.

The syntax for using the OpenDJ dsconfig command to create a new password policy can be seen below:

$ ./dsconfig create-password-policy --set default-password-storage-scheme:"Salted SHA-1" --set password-attribute:userpassword --set lockout-failure-count:3 
  --type password-policy --policy-name "Example Corp User Password Policy" 
  --hostname localhost --port 4444 --trustAll --bindDN cn="Directory Manager" 
  --bindPassword ****** --no-prompt
Note

This example contains a minimum number of settings (default-password-storage-scheme, password-attribute, and lockout-failure-count). Consider adding additional settings to customize your password policy as desired.

You can now assign the password policy to an individual user by adding the following attribute as a subentry to the user’s object:

ds-pwp-password-policy-dn:  cn=Example Corp User Password Policy,cn=Password Policies, cn=config

This can be performed using any LDAP client where you have write permissions to a user’s entry. The following example uses the ldapmodify command in an interactive mode to perform this operation:

$ ./ldapmodify -D "cn=Directory Manager" -w ****** <ENTER>
dn: uid=bnelson,ou=People,dc=example,dc=com <ENTER>
changetype: modify <ENTER>
replace: ds-pwp-password-policy-dn <ENTER>
ds-pwp-password-policy-dn: cn=Example Corp User Password Policy,
  cn=Password Policies,cn=config <ENTER>
<ENTER>

Another method of setting this password policy is through the use of a dynamically created virtual attribute (that is, one that is not persisted in the OpenDJ database backend). The following definition automatically assigns this new password policy to all users that exist beneath the ou=people container (the scope of the virtual attribute):

dn: cn=Example Corp User Password Policy Assignment,cn=Virtual Attributes,cn=config
objectClass: ds-cfg-virtual-attribute
objectClass: ds-cfg-user-defined-virtual-attribute
objectClass: top
ds-cfg-base-dn: ou=people,dc=example,dc=com
cn: Example Corp User Password Policy Assignment
ds-cfg-attribute-type: ds-pwp-password-policy-dn
ds-cfg-enabled: true
ds-cfg-java-class: org.opends.server.extensions.UserDefinedVirtualAttributeProvider
ds-cfg-filter: (objectclass=sbacperson)
ds-cfg-value: cn=Example Corp User Password Policy,cn=Password Policies,cn=config
Note

You can also use filters to create very granular results on how password polices are applied.

Configuring Account Lockout in OpenDJ has more flexibility and as such may be considered to be more powerful than OpenAM in this area. The potential confusion, however, comes when attempting to unlock a user’s account when they have been locked out of both OpenAM and OpenDJ. This is described in the following example.

A Deeper Dive into Account Lockout

Consider an environment where OpenAM is configured with the LDAP authentication module and that module has been configured to use an OpenDJ instance as the authentication database:

 

OpenAM and OpenDJ have both been configured to lock a user’s account after 3 invalid password attempts. What kind of behavior can you expect? Let’s walk through each step of an Account Lockout process and observe the behavior on Account Lockout specific attributes.

Step 1:  Query Account Lockout Specific Attributes for the Test User

$ ./ldapsearch -D "cn=Directory Manager" -w ****** uid=testuser1 inetuserstatus \
  sunAMAuthInvalidAttemptsData pwdFailureTime pwdAccountLockedTime

dn: uid=testuser1,ou=test,dc=example,dc=com
inetuserstatus: Active

The user is currently active and Account Lockout specific attributes are empty.

Step 2:  Open the OpenAM Console and access the login screen for the realm where Account Lockout has been configured.

Step 3:  Enter an invalid password for this user

Step 4:  Query Account Lockout Specific Attributes for the Test User

$ ./ldapsearch -D "cn=Directory Manager" -w ****** uid=testuser1 inetuserstatus \
  sunAMAuthInvalidAttemptsData pwdFailureTime pwdAccountLockedTime

dn: uid=testuser1,ou=test,dc=example,dc=com
sunAMAuthInvalidAttemptsData:: PEludmFsaWRQYXNzd29yZD48SW52YWxpZENvdW50PjE8L0
  ludmFsaWRDb3VudD48TGFzdEludmFsaWRBdD4xMzk4MTcxNTAwMDE4PC9MYXN0SW52YWxpZEF0P
  jxMb2NrZWRvdXRBdD4wPC9Mb2NrZWRvdXRBdD48QWN0dWFsTG9ja291dER1cmF0aW9uPjA8L0Fj
  dHVhbExvY2tvdXREdXJhdGlvbj48L0ludmFsaWRQYXNzd29yZD4=
inetuserstatus: Active
pwdFailureTime: 20140422125819.918Z

You now see that there is a value for the pwdFailureTime. This is the timestamp of when the first password failure occurred. This attribute was populated by OpenDJ.

The sunAMAuthInvalidAttemptsData attribute is populated by OpenAM. This is a base64 encoded value that contains valuable information regarding the invalid password attempt. Run this through a base64 decoder and you will see that this attribute contains the following information:

<InvalidPassword><InvalidCount>1</InvalidCount><LastInvalidAt>1398171500018
  </LastInvalidAt><LockedoutAt>0</LockedoutAt><ActualLockoutDuration>0
  </ActualLockoutDuration></InvalidPassword>

Step 5:  Repeat Steps 2 and 3. (This is the second password failure.)

Step 6:  Query Account Lockout Specific Attributes for the Test User

$ ./ldapsearch -D "cn=Directory Manager" -w ****** uid=testuser1 inetuserstatus \
  sunAMAuthInvalidAttemptsData pwdFailureTime pwdAccountLockedTime

dn: uid=testuser1,ou=test,dc=example,dc=com
sunAMAuthInvalidAttemptsData:: PEludmFsaWRQYXNzd29yZD48SW52YWxpZENvdW50PjI8L0
  ludmFsaWRDb3VudD48TGFzdEludmFsaWRBdD4xMzk4MTcxNTUzMzUwPC9MYXN0SW52YWxpZEF0P
  jxMb2NrZWRvdXRBdD4wPC9Mb2NrZWRvdXRBdD48QWN0dWFsTG9ja291dER1cmF0aW9uPjA8L0Fj
  dHVhbExvY2tvdXREdXJhdGlvbj48L0ludmFsaWRQYXNzd29yZD4=
inetuserstatus: Active
pwdFailureTime: 20140422125819.918Z
pwdFailureTime: 20140422125913.151Z

There are now two values for the pwdFailureTime attribute – one for each password failure. The sunAMAuthInvalidAttemptsData attribute has been updated as follows:

<InvalidPassword><InvalidCount>2</InvalidCount><LastInvalidAt>1398171553350
  </LastInvalidAt><LockedoutAt>0</LockedoutAt><ActualLockoutDuration>0
  </ActualLockoutDuration></InvalidPassword>

Step 7:  Repeat Steps 2 and 3.  (This is the third and final password failure.)

OpenAM displays an error message indicating that the user’s account has been locked.

Step 8:  Query Account Lockout Specific Attributes for the Test User

$ ./ldapsearch -D "cn=Directory Manager" -w ****** uid=testuser1 inetuserstatus \ 
  sunAMAuthInvalidAttemptsData pwdFailureTime pwdAccountLockedTime

dn: uid=testuser1,ou=test,dc=example,dc=com
sunAMAuthInvalidAttemptsData:: PEludmFsaWRQYXNzd29yZD48SW52YWxpZENvdW50PjA8L0
  ludmFsaWRDb3VudD48TGFzdEludmFsaWRBdD4wPC9MYXN0SW52YWxpZEF0PjxMb2NrZWRvdXRBd
  D4wPC9Mb2NrZWRvdXRBdD48QWN0dWFsTG9ja291dER1cmF0aW9uPjA8L0FjdHVhbExvY2tvdXRE
  dXJhdGlvbj48L0ludmFsaWRQYXNzd29yZD4=
inetuserstatus: Inactive
pwdFailureTime: 20140422125819.918Z
pwdFailureTime: 20140422125913.151Z
pwdFailureTime: 20140422125944.771Z
pwdAccountLockedTime: 20140422125944.771Z

There are now three values for the pwdFailureTime attribute – one for each password failure. The sunAMAuthInvalidAttemptsData attribute has been updated as follows:

<InvalidPassword><InvalidCount>0</InvalidCount><LastInvalidAt>0</LastInvalidAt>
  <LockedoutAt>0</LockedoutAt><ActualLockoutDuration>0</ActualLockoutDuration>
  </InvalidPassword>

You will note that the counters have all been reset to zero. That is because the user’s account has been inactivated by OpenAM by setting the value of the inetuserstatus attribute to Inactive. Additionally, the third invalid password caused OpenDJ to lock the account by setting the value of the pwdAccountLockedTime attribute to the value of the last password failure.

Now that the account is locked out, how do you unlock it? The natural thing for an OpenAM administrator to do is to reset the value of the inetuserstatus attribute and they would most likely use the OpenAM Console to do this as follows:

The problem with this approach is that while the user’s status in OpenAM is now made active, the status in OpenDJ remains locked.

$ ./ldapsearch -D "cn=Directory Manager" -w ****** uid=testuser1 inetuserstatus \
  sunAMAuthInvalidAttemptsData pwdFailureTime pwdAccountLockedTime

dn: uid=testuser1,ou=test,dc=example,dc=com
sunAMAuthInvalidAttemptsData:: PEludmFsaWRQYXNzd29yZD48SW52YWxpZENvdW50PjA8L0
  ludmFsaWRDb3VudD48TGFzdEludmFsaWRBdD4wPC9MYXN0SW52YWxpZEF0PjxMb2NrZWRvdXRBd
  D4wPC9Mb2NrZWRvdXRBdD48QWN0dWFsTG9ja291dER1cmF0aW9uPjA8L0FjdHVhbExvY2tvdXRE
  dXJhdGlvbj48L0ludmFsaWRQYXNzd29yZD4=
inetuserstatus: Active
pwdFailureTime: 20140422125819.918Z
pwdFailureTime: 20140422125913.151Z
pwdFailureTime: 20140422125944.771Z
pwdAccountLockedTime: 20140422125944.771Z

Attempting to log in to OpenAM with this user’s account yields an authentication error that would make most OpenAM administrators scratch their head; especially after just resetting the user’s status.

The trick to fixing this is to clear the pwdAccountLockedTime and pwdFailureTime attributes and the way to do this is by modifying the user’s password. Once again, the ldapmodify command can be used as follows:

$ ./ldapmodify -D "cn=Directory Manager" -w ****** <ENTER>
dn: uid=testuser1,ou=test,dc=example,dc=com <ENTER>
changetype: modify <ENTER>
replace: userPassword <ENTER>
userPassword: newpassword <ENTER>
<ENTER>
$ ./ldapsearch -D "cn=Directory Manager" -w ****** uid=testuser1 inetuserstatus \
  sunAMAuthInvalidAttemptsData pwdFailureTime pwdAccountLockedTime

dn: uid=testuser1,ou=test,dc=example,dc=com
sunAMAuthInvalidAttemptsData:: PEludmFsaWRQYXNzd29yZD48SW52YWxpZENvdW50PjA8L0
  ludmFsaWRDb3VudD48TGFzdEludmFsaWRBdD4wPC9MYXN0SW52YWxpZEF0PjxMb2NrZWRvdXRBd
  D4wPC9Mb2NrZWRvdXRBdD48QWN0dWFsTG9ja291dER1cmF0aW9uPjA8L0FjdHVhbExvY2tvdXRE
  dXJhdGlvbj48L0ludmFsaWRQYXNzd29yZD4=
inetuserstatus: Active
pwdChangedTime: 20140422172242.676Z

This, however, requires two different interfaces for managing the user’s account. An easier method is to combine the changes into one interface. You can modify the inetuserstatus attribute using ldapmodify or if you are using the OpenAM Console, simply change the password while you are updating the user’s status.

There are other ways to update one attribute by simply modifying the other. This can range in complexity from a simple virtual attribute to a more complex yet powerful custom OpenDJ plugin. But in the words of Voltaire, “With great power comes great responsibility.”

So go forth and wield your new found power; but do it in a responsible manner.

Contributed by Bill Nelson (idmdude)

See Also

idmdude - Understanding OpenAM and OpenDJ Account Lockout Behaviors

How do I enable account lockout in AM/OpenAM (All versions)?

How do I unlock a user's account using the REST API in AM/OpenAM (All versions)?

FAQ: Passwords in DS/OpenDJ

OpenAM Administration Guide › Defining Authentication Services › Configuring Account Lockout

OpenDJ Administration Guide › Configuring Password Policy


How do I change the admin account password used for replication in DS/OpenDJ (All versions)?

The purpose of this article is to provide information on changing the admin account password used for replication in DS/OpenDJ.

Changing the admin account password used for replication

There is no default admin account password used for replication. When you enable replication for the first time using the dsreplication configure (DS 5 and later) or dsreplication enable (pre-DS 5)  command, you set the password to be used for this account. This user is created in the cn=admin data backend, which is replicated across all servers.

You can change the password using a standard LDAP operation. For example, you would use a command such as the following if the user was 'admin':

$ ./ldappasswordmodify --bindDN "cn=Directory Manager" --bindPassword password --port 4444 --newPassword Passw0rd --authzID "cn=admin,cn=Administrators,cn=admin data" --trustAll --useSSL

You can then test the new password with the dsreplication status command, for example:

$ ./dsreplication status --adminUID admin --adminPassword Passw0rd --hostname ds1.example.com --port 4444 --trustAll

See Also

FAQ: Passwords in DS/OpenDJ

Troubleshooting DS/OpenDJ

Administration Guide › Troubleshooting Server Problems › Resetting Administrator Passwords

Reference › Tools Reference › dsreplication

Related Training

N/A

Related Issue Tracker IDs

N/A


How do I add multiple values for the same password attribute using different hashing algorithms in DS/OpenDJ (All versions)?

The purpose of this article is to provide information on adding multiple values for the same password attribute using different hashing algorithms in DS/OpenDJ. This article assumes you are using the userPassword attribute for passwords, but the process still applies if you are using a different attribute for passwords.

Adding multiple values

You can add multiple values as follows:

  1. Encode a password using your first required hashing algorithm, for example SSHA:
    $ ./encode-password --storageScheme SSHA --clearPassword password1
    Example output:
    {SSHA}71w+3JjdTWKH3NuKHBpvzUqPmyvXfG9fAXRvtg==
    
  2. Encode a password using your second required hashing algorithm, for example SSHA256:
    $ ./encode-password --storageScheme SSHA256 --clearPassword password2
    Example output:
    {SSHA256}roHY0/rfiO8+q/DZ+km9UG2UiL7AO6RuQ4oFefRDmEYHbeBEkNzmaQ==
    
  3. Add these encoded passwords to a user entry and import or add:
    dn: uid=user.2000,ou=People,dc=example,dc=com
    objectClass: person
    objectClass: inetorgperson
    objectClass: organizationalperson
    objectClass: top
    postalAddress: 1000 The Street, Cincinnati, MA  50563
    postalCode: 50563
    uid: user.2000
    description: This is a description.
    userPassword: {SSHA}71w+3JjdTWKH3NuKHBpvzUqPmyvXfG9fAXRvtg==
    userPassword: {SSHA256}roHY0/rfiO8+q/DZ+km9UG2UiL7AO6RuQ4oFefRDmEYHbeBEkNzmaQ==
    employeeNumber: 2000
    initials: JD
    givenName: John
    pager: +1 234 567 8910
    mobile: +1 019 876 5432
    cn: John Doe
    telephoneNumber: +1 012 345 6789
    sn: Doe
    street: 1000 The Street
    homePhone: +1 987 654 3210
    mail: user.2000@example.com
    l: Cincinnati
    st: MA
    
  4. Bind using this user entry and each encoded password:
    $ ./ldapsearch --port1389 --hostname localhost --baseDN "dc=example,dc=com" --bindDN "uid=user.2000,ou=People,dc=example,dc=com" --bindPassword password1 "(uid=user.2000)" userPassword
    dn: uid=user.2000,ou=People,dc=example,dc=com
    userPassword: {SSHA}71w+3JjdTWKH3NuKHBpvzUqPmyvXfG9fAXRvtg==
    userPassword: {SSHA256}roHY0/rfiO8+q/DZ+km9UG2UiL7AO6RuQ4oFefRDmEYHbeBEkNzmaQ==
    
    $ ./ldapsearch --port1389 --hostname localhost --baseDN "dc=example,dc=com" --bindDN "uid=user.2000,ou=People,dc=example,dc=com" --bindPassword password2 "(uid=user.2000)" userPassword
    dn: uid=user.2000,ou=People,dc=example,dc=com
    userPassword: {SSHA}71w+3JjdTWKH3NuKHBpvzUqPmyvXfG9fAXRvtg==
    userPassword: {SSHA256}roHY0/rfiO8+q/DZ+km9UG2UiL7AO6RuQ4oFefRDmEYHbeBEkNzmaQ==

See Also

FAQ: Installing and configuring DS/OpenDJ

FAQ: Passwords in DS/OpenDJ

How does DS/OpenDJ (All versions) store password values?

Administration Guide › Configuring Password Policy

Reference › encode-password

Related Training

ForgeRock Directory Services Core Concepts

Related Issue Tracker IDs

N/A


How do I change a password storage scheme and apply a new password policy to users in DS/OpenDJ (All versions)?

The purpose of this article is to provide assistance if you want to migrate users to a new password policy in DS/OpenDJ. It also provides information on changing the storage scheme associated with a password policy (for example, from SHA-256 to SHA-512), without applying a new policy.

Overview

If you want to change the storage scheme associated with your password policy, you do not have to create a new password policy to achieve this nor do users necessarily have to change their passwords.

You have the following options if you want to change the storage scheme (without creating a new policy):

The option you choose depends on the vulnerability of your existing storage scheme. If you have any concerns, you will want users to change their password anyway, in which case the first option is the most suitable. If you are changing the storage scheme for other reasons, the second option may be suitable and less disruptive.

If you want to change the policy applied to users, you have the following options:

Attributes

The following attributes are referred to in this article:

Attribute  Meaning
pwdPolicySubentry This attribute indicates which password policy applies to the user.
ds-pwp-password-policy-dn This attribute is used to assign password policies to any given user or group of users.
collectAttributeSubentries This attribute is used to set the ds-pwp-password-policy-dn for group member entries.

Changing the storage scheme associated with a server based policy

You can change the storage scheme associated with a server based password policy as follows:

  1. Update the storage scheme associated with your existing password policy using a dsconfig command such as this:
    $ ./dsconfig set-password-policy-prop --policy-name "Current Password Policy" --set default-password-storage-scheme:"Salted SHA-512" --hostname ds1.example.com --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --trustAll --no-prompt
    
    Once the policy is updated, authentication will continue to work seamlessly since the passwords themselves are unchanged. All newly created and updated passwords will be stored using the new storage scheme.
  2. Force all users to update their passwords to ensure they are stored using the new scheme. An administrator can reset users' passwords, and assuming the force-change-on-reset property is set to true, users would have to change their password the next time they login. See Administration Guide › Require Password Change on Add or Reset for further information on this setting.

Testing

The following process demonstrates how to check that the new storage scheme is used once the user's password has been changed:

  1. Search the userPassword with a ldapsearch command to confirm that the user still has their password stored under the existing storage scheme (SSHA256 in this example):
    $ ./ldapsearch --port 1389 --bindDN "uid=user.1,ou=people,dc=example,dc=com" --bindPassword changeit --baseDN dc=example,dc=com "uid=user.1" userPassword
    
    dn: uid=user.1,ou=People,dc=example,dc=com
    userPassword: {SSHA256}nGoaDykq4sQl0TdURXGGk9HcwXwLPQsqPMWq320uxARbov5qT/BWRg==
    
  2. Update the user's password using a ldappasswordmodify command:
    $ ./ldappasswordmodify --port 1389 --bindDN "uid=user.1,ou=people,dc=example,dc=com" --bindPassword changeit --newPassword strongPassw0rd 
    
    modify operation was successful
  3. Search the userPassword again to verify that the user's password is now stored under the new storage scheme (SSHA512 in this example):
    $ ./ldapsearch --port 1389 --bindDN "uid=user.1,ou=people,dc=example,dc=com" --bindPassword password --baseDN dc=example,dc=com "uid=user.1" userPassword
    
    dn: uid=user.1,ou=People,dc=example,dc=com
    userPassword: {SSHA512}bf3lzOwsN1WoMPjuozaQiQsMc7jxnru/DYd6ah/nsugzWCZlbnhFdOgWEZJw7JjUUvvVOtCXiM11jSWc3vrVM
    

Deprecating the old storage scheme and applying a new one (server based policy)

You can deprecate the old storage scheme as follows:

  1. Deprecate the storage scheme associated with your existing server based password policy and set the new storage scheme using a dsconfig command such as this:
    $ ./dsconfig set-password-policy-prop --policy-name "Current Password Policy" --set deprecated-password-storage-scheme:"Salted SHA-256" --set default-password-storage-scheme:"Salted SHA-512" --hostname ds1.example.com --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --trustAll --no-prompt
    Once the user successfully authenticates, their password is stored under the new storage scheme.

Testing

The following process demonstrates how to check that the new storage scheme is used for a user's password once they have authenticated:

  1. Search the userPassword with a ldapsearch command to confirm that the user still has their password stored under the existing storage scheme (SSHA256 in this example):
    $ ./ldapsearch --port 1389 --bindDN "uid=user.1,ou=people,dc=example,dc=com" --bindPassword changeit --baseDN dc=example,dc=com "uid=user.1" userPassword
    
    dn: uid=user.1,ou=People,dc=example,dc=com
    userPassword: {SSHA256}nGoaDykq4sQl0TdURXGGk9HcwXwLPQsqPMWq320uxARbov5qT/BWRg==
    
  2. Authenticate as the user using their existing password. Do not change their password.
  3. Search the userPassword again to verify that the user's password is now stored under the new storage scheme (SSHA512 in this example):
    $ ./ldapsearch --port 1389 --bindDN "uid=user.1,ou=people,dc=example,dc=com" --bindPassword password --baseDN dc=example,dc=com "uid=user.1" userPassword
    
    dn: uid=user.1,ou=People,dc=example,dc=com
    userPassword: {SSHA512}KeXfZptAT9AQlm1gaL4Rb3ChWdX2Md3Cps67bj3BuTRfL2PbZ5TTT4Z6bbM5R0

Using the pwdPolicy object class to assign subentry based password policies

The following process demonstrates applying a subentry based password policy to all members of a branch, with the following example values (where both the policy and users are directly under: ou=People,dc=example,dc=com):

  • The policy DN is:
    cn=SSHA512 PP,ou=People,dc=example,dc=com
  • The users are in the following branch:
    ou=People,dc=example,dc=com

You can apply a password policy to all your users as follows 

  1. Create a ldif file for the new custom password policy. For example:
    $ cat new-policy.ldif 
    dn: cn=SSHA512 PP,dc=example,dc=com
    objectClass: top
    objectClass: subentry
    objectClass: pwdPolicy
    cn: SSHA512 PP
    pwdAttribute: userPassword
    [..., etc]
    subtreeSpecification: {base "ou=people", specificationFilter
      "(isMemberOf=cn=Directory Administrators,ou=Groups,dc=example,dc=com)" }
  2. Apply the new policy using the following ldapmodify command depending on your version:
    • DS 5 and later:
      $ ./ldapmodify --port 1389 --bindDN "cn=Directory Manager" --bindPassword password new-policy.ldif
    • Pre-DS 5:
      $ ./ldapmodify --defaultAdd --port 1389 --bindDN "cn=Directory Manager" --bindPassword password --filename new-policy.ldif
    You will see the following response:
    Processing ADD request for cn=SSHA512 PP,ou=People,dc=example,dc=com
    ADD operation successful for DN cn=SSHA512 PP,ou=People,dc=example,dc=com

Testing

The following process demonstrates how to check that the new password policy has been applied and that the storage scheme is updated once the user's password has been changed:

  1. Verify that the new password policy has been applied, for example:
    $ ./ldapsearch --port 1389 --bindDN "cn=Directory Manager" --bindPassword password --baseDN dc=example,dc=com 'uid=user.1' pwdPolicySubentry + userPassword
    
    dn: uid=user.1,ou=People,dc=example,dc=com
    userPassword: {SSHA}x8MQVlR06gUOPwz+yP1TcJElUI1O0bwuSVyQ+A==
    pwdPolicySubentry: cn=SSHA512 PP,dc=example,dc=com
    entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4
    etag: 000000003e23b18a
    structuralObjectClass: inetOrgPerson
    numSubordinates: 0
    collectiveAttributeSubentries: cn=SSHA512 Password Policy for all Users,dc=example,dc=com
    hasSubordinates: false
    subschemaSubentry: cn=schema
    entryDN: uid=user.1,ou=People,dc=example,dc=com
    ds-pwp-password-policy-dn: cn=SSHA512 PP,dc=example,dc=com
    Notice that the password has not yet updated to the new SSHA512 storage scheme but the new password policy has been applied.
  2. Update the user's password using a ldappasswordmodify command:
    $ ./ldappasswordmodify --port 1389 --bindDN "uid=user.1,ou=people,dc=example,dc=com" --bindPassword changeit --newPassword strongPassw0rd 
    
    modify operation was successful
  3. Search the userPassword again to verify that the user's password is now stored under the new storage scheme (SSHA512 in this example):
    $ ./ldapsearch --port 1389 --bindDN "cn=Directory Manager" --bindPassword password --baseDN dc=example,dc=com "uid=user.1" userPassword
    
    dn: uid=user.1,ou=People,dc=example,dc=com
    userPassword: {SSHA512}bf3lzOwsN1WoMPjuozaQiQsMc7jxnru/DYd6ah/nsugzWCZlbnhFdOgWEZJw7JjUUvvVOtCXiM11jSWc3vrVM

Using a collectiveAttributeSubentry object to assign password policies

The process you need to follow depends on whether you currently have the Default Password Policy assigned to your users or a custom one; you can check the pwdPolicySubentry entry using a ldapsearch command, for example:

$ ./ldapsearch --port 1389 --bindDN "cn=Directory Manager" --bindPassword password --baseDN dc=example,dc=com 'uid=user.1' pwdPolicySubentry + userPassword

dn: uid=user.1,ou=People,dc=example,dc=com
userPassword: {SSHA256}nGoaDykq4sQl0TdURXGGk9HcwXwLPQsqPMWq320uxARbov5qT/BWRg==
pwdPolicySubentry: cn=Default Password Policy,cn=Password Policies,cn=config
entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4
etag: 000000003e23b18a
structuralObjectClass: inetOrgPerson
numSubordinates: 0
hasSubordinates: false
subschemaSubentry: cn=schema
entryDN: uid=user.1,ou=People,dc=example,dc=com

If you already have a custom password policy (applied using collective attributes), you will see a collectiveAttributeSubentries entry in response to the ldapsearch, for example:

collectiveAttributeSubentries: cn=Custom Password Policy for all Users,dc=example,dc=com

You should then follow the appropriate process below:

Note

If you already have a custom policy applied, you can remove it first and then follow the new password policy procedure if you prefer. You can remove it using an ldapdelete command or the control panel (pre-DS 6 only) (Select Manage Entries, navigate to the required branch, select the password policy and then select Delete Entry). The control panel is removed in DS 6.

An example ldapdelete command is:

$ ./ldapdelete --port 1389 --bindDN "cn=Directory Manager" --bindPassword password cn=<password policy>,dc=example,dc=com

Apply a new password policy 

The following process demonstrates applying a new password policy to all users under the base ou=People:

  1. Create a new custom password policy. See Administration Guide › Configuring Password Policies for further information.
  2. Apply the new password policy to all users under dn: ou=People,dc=example,dc=com as follows:
    1. Create a ldif file with the changes, for example:
      dn: cn=SSHA512 Password Policy for all Users,dc=example,dc=com
      objectClass: collectiveAttributeSubentry
      objectClass: extensibleObject
      objectClass: subentry
      objectClass: top
      cn: SSHA512 Password Policy for all Users
      ds-pwp-password-policy-dn;collective: cn=SSHA512 PP,cn=Password Policies,cn=config
      subtreeSpecification: {base "ou=people", specificationFilter
        "(isMemberOf=cn=Directory Administrators,ou=Groups,dc=example,dc=com)" }
    2. Apply this update using the following ldapmodify command depending on your version:
      • DS 5 and later:
        $ ./ldapmodify --port 1389 --bindDN "cn=Directory Manager" --bindPassword password pwp-policy.ldif
      • Pre-DS 5:
        $ ./ldapmodify --defaultAdd --port 1389 --bindDN "cn=Directory Manager" --bindPassword password --filename pwp-policy.ldif
      You will see the following response:
      Processing ADD request for cn=SSHA512 Password Policy for all Users,dc=example,dc=com
      ADD operation successful for DN cn=SSHA512 Password Policy for all Users,dc=example,dc=com 

Change the password policy 

The following process demonstrates changing the password policy applicable to all users under the base ou=People:

  1. Create a new custom password policy. See Administration Guide › Configuring Password Policies for further information.
  2. Change the password policy to the new policy for all users under dn: ou=People,dc=example,dc=com as follows:
    1. Create a ldif file with the change, for example:
      $ cat change-policy.ldif 
      dn: cn=Custom Password Policy for all Users,dc=example,dc=com
      changetype: modify
      replace: ds-pwp-password-policy-dn;collective
      ds-pwp-password-policy-dn;collective: cn=SSHA512 PP,cn=Password Policies,cn=config
    2. Apply this update using the following ldapmodify command depending on your version:
      • DS 5 and later:
        $ ./ldapmodify --port 1389 --bindDN "cn=Directory Manager" --bindPassword password change-policy.ldif
      • Pre-DS 5:
        $ ./ldapmodify --port 1389 --bindDN "cn=Directory Manager" --bindPassword password --filename change-policy.ldif
      You will see the following response:
      Processing MODIFY request for cn=Custom Password Policy for all Users,dc=example,dc=com
      MODIFY operation successful for DN cn=Custom Password Policy for all Users,dc=example,dc=com

Testing

The following process demonstrates how to check that the new password policy has been applied and that the storage scheme is updated once the user's password has been changed:

  1. Verify that the new password policy has been applied, for example:
    $ ./ldapsearch --port 1389 --bindDN "cn=Directory Manager" --bindPassword password --baseDN dc=example,dc=com 'uid=user.1' pwdPolicySubentry + userPassword
    
    dn: uid=user.1,ou=People,dc=example,dc=com
    userPassword: {SSHA}x8MQVlR06gUOPwz+yP1TcJElUI1O0bwuSVyQ+A==
    pwdPolicySubentry: cn=SSHA512 PP,cn=Password Policies,cn=config
    entryUUID: ad55a34a-763f-358f-93f9-da86f9ecd9e4
    etag: 000000003e23b18a
    structuralObjectClass: inetOrgPerson
    numSubordinates: 0
    collectiveAttributeSubentries: cn=SSHA512 Password Policy for all Users,dc=example,dc=com
    hasSubordinates: false
    subschemaSubentry: cn=schema
    entryDN: uid=user.1,ou=People,dc=example,dc=com
    ds-pwp-password-policy-dn: cn=SSHA512 PP,cn=Password Policies,cn=config
    Notice that the password has not yet updated to the new SSHA512 storage scheme but the new password policy has been applied.
  2. Update the user's password using a ldappasswordmodify command:
    $ ./ldappasswordmodify --port 1389 --bindDN "uid=user.1,ou=people,dc=example,dc=com" --bindPassword changeit --newPassword strongPassw0rd 
    
    modify operation was successful
  3. Search the userPassword again to verify that the user's password is now stored under the new storage scheme (SSHA512 in this example):
    $ ./ldapsearch --port 1389 --bindDN "cn=Directory Manager" --bindPassword password --baseDN dc=example,dc=com "uid=user.1" userPassword
    
    dn: uid=user.1,ou=People,dc=example,dc=com
    userPassword: {SSHA512}bf3lzOwsN1WoMPjuozaQiQsMc7jxnru/DYd6ah/nsugzWCZlbnhFdOgWEZJw7JjUUvvVOtCXiM11jSWc3vrVM
Note

If you have changed the password policy, the name on the collectiveAttribute will remain the same after the new policy has been applied.

See Also

How does DS/OpenDJ (All versions) store password values?

How do I improve performance when using the PBKDF2 storage scheme in DS/OpenDJ (All versions)?

How does password expiration work in DS/OpenDJ (All versions)?

How do I add multiple values for the same password attribute using different hashing algorithms in DS/OpenDJ (All versions)?

FAQ: Passwords in DS/OpenDJ

Passwords in DS/OpenDJ

Administration Guide › About OpenDJ Password Policies

Administration Guide › Configuring Password Storage

Administration Guide › Assigning Password Policies

Administration Guide › Sample Password Policies

Assigning a Custom Password policy to a subTree

Related Training

N/A

Related Issue Tracker IDs

N/A


FAQ: Passwords in DS/OpenDJ

The purpose of this FAQ is to provide answers to commonly asked questions regarding passwords in DS/OpenDJ.

Frequently asked questions

Q. Can I change the password policy that applies to a user?

A. Yes, as a DS/OpenDJ administrator, you can change the password policy that applies to a user.

See Administration Guide › Assigning Password Policies for further information.

Q. Does DS/OpenDJ have a maximum password length?

A. The password length in DS/OpenDJ is determined by the password scheme being used. For example, the UNIX® crypt scheme only uses the first 8 characters for compatibility, whereas the SHA1 scheme (like most schemes) is not limited.

Q. What is the default password hashing scheme used in DS/OpenDJ?

A. SHA1 is the default hashing scheme for passwords in DS/OpenDJ. The salt is an 8 bytes (64-bit) random string and is used with the password to produce the 20 bytes message digest. However, DS/OpenDJ directory server supports a wide range of password hashing schemes and salted SHA512 is currently the most secure hashing algorithm we support (and the salt here is also an 8 bytes (64-bit) random octet string).

See How does DS/OpenDJ (All versions) store password values? and More secure passwords! for more information on password hashing in DS/OpenDJ.

Q. Can I import an LDIF file that contains pre-encoded passwords?

A. Yes, you can by setting the advanced password policy property: allow-pre-encoded-passwords. You can set this using dsconfig, for example:

$ ./dsconfig set-password-policy-prop --policy-name "Default Password Policy" --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --advanced --set allow-pre-encoded-passwords:true --trustAll --no-prompt
Note

The pre-encoded passwords in the LDIF file must be base64 encoded.

If you try to import an LDIF file with pre-encoded passwords without setting this property to true, you will see the following error:

Pre-encoded passwords are not allowed for the password attribute userPassword

Q. Does DS/OpenDJ support using multiple attributes for a password, for example, to allow different hashing algorithms?

A. No, DS/OpenDJ only uses one attribute for a password, but you can configure which attribute is used, for example, userPassword. Additionally, you can have multiple values for this attribute, with each value having a different hashing algorithm as described in How do I add multiple values for the same password attribute using different hashing algorithms in DS/OpenDJ (All versions)?

See Administration Guide › Configuring Password Policy for further information.

Q. Is the MD4 hashing algorithm supported for passwords in DS/OpenDJ?

A. No, the MD4 hashing algorithm is no longer available for use in DS/OpenDJ; it has been severely compromised and is not recommended for use.

Q. When an account is locked, when are the lockout attributes (pwdAccountLockedTime and pwdFailureTime) reset?

A. These lockout attributes remain set until the user attempts to log in. If the user tries to log in within the lockout duration, they are denied access, but if they log in after the lockout duration has passed, their account is unlocked and the lockout attributes are reset.

See Understanding OpenAM 12.x, 13.x and OpenDJ 2.6.x, 3.x account lockout behaviors for further information.

Q. What happens to a user's max-password-age property if the property is subsequently updated?

A. Typically the user's max-password-age property is updated so that the remaining time until expiration stays the same. 

However, the calculation can be further complicated depending on the configuration and status of the expiration warning. For example, if the user has 10 days left and you have a 5 day warn time - then you change the maximum age from 40 to 30 days. In theory you would think you have 0 days left, but what would actually happen is the user would get their warning and then have a further 5 days until expiration.

Q. Can I define sub-entry based password validators or generators?

A. No, DS/OpenDJ does not currently support sub-entry based password validators or generators. An RFE exists for this: OPENDJ-286 (Finish implementation of password policy sub-entry support).

Q. Does DS/OpenDJ support the two Behera password policy attributes to delay response for failed authentication: pwdMinDelay and pwdMaxDelay?

A. No. These attributes are introduced in version 10 of the Internet-Draft Password Policy for LDAP Directories and DS/OpenDJ supports version 09.

Q. Where does DS/OpenDJ define truststore files and passwords?

A. Truststores are used for public, signed certificates but there are also keystores, which are used for private keys.

By default, truststore and/or keystore files and passwords are stored in different stores depending on their purpose:

  • ads-truststore for replication purposes.
  • admin-truststore and admin-keystore for administration purposes.
  • truststore and keystore for everything else.

These files are located in /path/to/ds/config; as of DS 6 and later, ads-truststore is located in /path/to/ds/db/ads-truststore for new installs.

See Administration Guide › Changing Server Certificates for further information.

Q. Can I decrypt a currently stored password?

A. No. Non-clear text passwords in DS/OpenDJ are encrypted or hashed and cannot be reverse-engineered through any DS/OpenDJ related process or by ForgeRock. For example a password hashed using the Salted SHA-1 scheme is stored as follows:

userPassword: {SSHA}wcs5GK3evrRF8Y+zsmTQPGlgZ3CPaJIyYwVhhQ==

When a user authenticates, the given clear text password is processed using the same algorithms. DS/OpenDJ compares the authentication password to the the stored userPassword version. If the comparison is true, the user is authenticated.

See Also

How does DS/OpenDJ (All versions) store password values?

FAQ: Installing and configuring DS/OpenDJ

FAQ: Upgrading DS/OpenDJ

FAQ: General DS/OpenDJ

Passwords in DS/OpenDJ

Related Training

ForgeRock Directory Services Core Concepts


Password Synchronization Plugin


How do I upgrade DS/OpenDJ (All versions) if I have the DS/OpenDJ Password Sync Plugin for IDM/OpenIDM installed?

The purpose of this article is to provide information on upgrading DS/OpenDJ if you have the DS/OpenDJ Password Sync Plugin for IDM/OpenIDM installed.

Background Information

The password plugin configuration is specified in one of the following files depending on the plugin version:

  • Plugin version 3.5.0 and later - openidm-accountchange-plugin-sample-config
  • Plugin versions 1.0.3 and 1.1.1 - openidm-pwsync-plugin-config.ldif
Note

You must use the plugin version that corresponds to your DS/OpenDJ version. See Release Notes › Supported Connectors, Connector Servers, and Plugins for further information.

Upgrading DS/OpenDJ

You can upgrade DS/OpenDJ as follows:

  1. Back up the password plugin configuration file (openidm-pwsync-plugin-config.ldif or openidm-accountchange-plugin-sample-config, which is located in the /path/to/ds/config directory) as this contains your current configuration details.
  2. Update the Default Password Policy to remove the account-status-notification-handler attribute using a dsconfig command such as the following:
    $ ./dsconfig set-password-policy-prop --policy-name "Default Password Policy" --reset account-status-notification-handler --hostname localhost --port 4444 --bindDn "cn=Directory Manager" --bindPassword password --trustAll --no-prompt
  3. Remove the changes made to the cn=config backend when the plugin was installed using a ldapdelete command such as the following:
    $ ./ldapdelete --hostname ds1.example.com --port 4444 --bindDN "cn=Directory Manager" --bindPassword password "cn=OpenIDM Notification Handler,cn=Account Status Notification Handlers,cn=config"
  4. Stop the DS/OpenDJ instance.
  5. Delete the following files that apply to the existing plugin:
    • openidm-pwsync-plugin-config.ldif or openidm-accountchange-plugin-sample-config in the /path/to/ds/config directory.
    • 90-openidm-pwsync-plugin.ldif file in the /path/to/ds/config/schema directory; as of DS 6 and later, located in /path/to/ds/db/schema for new installs.
    • opendj-accountchange-handler-x.x.x.jar in the /path/to/ds/lib/extensions directory.
  6. Restart the DS/OpenDJ instance.
  7. Upgrade to the new version of DS/OpenDJ. See Installation Guide and Upgrading DS/OpenDJ for further information.
  8. Install the new plugin per the instructions in Password Synchronization Plugin Guide › Installing the DS Password Synchronization Plugin.
  9. Update the configuration file (openidm-pwsync-plugin-config.ldif or openidm-accountchange-plugin-sample-config depending on which version of the plugin you installed) with the details contained in the configuration file you backed up if you want to retain the same behavior you had previously.

See Also

DS/OpenDJ (All versions) fail to start after upgrade if you use the Password Sync Plugin for IDM/OpenIDM

OpenDJ Password Synchronization Plugin 1.0.3 install fails on OpenDJ 3.x

How do I troubleshoot the DS/OpenDJ Password Synchronization plugin in IDM/OpenIDM (All versions)?

Password Synchronization Plugin Guide › Synchronizing Passwords With ForgeRock Directory Services (DS)

Related Training

N/A

Related Issue Tracker IDs

N/A


How do I troubleshoot the DS/OpenDJ Password Synchronization plugin in IDM/OpenIDM (All versions)?

The purpose of this article is to provide assistance if you are experiencing issues with the DS/OpenDJ password sync plugin in IDM/OpenIDM. This article also includes information on enabling debug logging for just the DS/OpenDJ password sync plugin to minimize the size of the resulting DS/OpenDJ logs.

Overview

This article includes the following details that you should check (and rectify) in order to troubleshoot the DS/OpenDJ password sync plugin in IDM/OpenIDM:

Note

If you are still unable to solve your issues, you should attach the collected log files and outputs when you raise a ticket to enable us to help you more quickly. See Sending troubleshooting data to ForgeRock Support for analysis for further information.

DS/OpenDJ password sync flow

The expected DS/OpenDJ password sync plugin flow is as follows:

  1. DS/OpenDJ receives an operation to modify an account password.
  2. The DS/OpenDJ password sync plugin is invoked and passes the cleartext password.
  3. The DS/OpenDJ password sync plugin encrypts the password and performs a Patch operation against the corresponding IDM/OpenIDM managed user's ldapPassword attribute.
  4. The onUpdate trigger within managed.json assigns the decrypted ldapPassword attribute value to the password attribute.
  5. The Implicit sync process is triggered for the managed user to LDAP mapping.
  6. The condition associated with the password attribute mapping is evaluated and returns false as the cleartext ldapPassword and password attribute values are the same.
  7. The LDAP Connector does not perform an update to DS/OpenDJ as there are no modifications to be performed.

Avoiding loops

To ensure this flow is followed correctly and you do not end up with a loop whereby the password change originating from DS/OpenDJ results in an update from IDM/OpenIDM back to DS/OpenDJ for the same LDAP entry, the condition associated with the password attribute mapping (sync.json file) should be similar to the following:

{
    "source" : "password",                  
    "condition" : {
        "type" : "text/javascript",
        "source" : "object.ldapPassword != null && (openidm.decrypt(object.password) != openidm.decrypt(object.ldapPassword))"
},

This mapping applies a condition to the sync of the password attribute, which is dependent upon the managed user's ldapPassword attribute value being different from the password attribute value.

Note

The ldapPassword attribute referred to above is configurable and may be different in your configuration; however, the DS/OpenDJ password sync plugin must be configured to Patch an attribute other than password when performing a bi-directional sync else it is impossible to determine whether an update is needed or not.

If DS/OpenDJ syncs passwords with IDM/OpenIDM in both directions, you need to ensure you have configured IDM/OpenIDM correctly to avoid an infinite loop. See Password Synchronization Plugin Guide › Preventing Infinite Loops for further information.

Keystores and certificates

Incorrect keystore and certificate details can cause issues with the DS/OpenDJ password sync plugin (some of which have been shown in the IDM/OpenIDM log section below). The key things to check are:

  1. The IDM/OpenIDM certificate has been imported into the DS/OpenDJ truststore.
  2. The DS/OpenDJ certificate has been imported into the IDM/OpenIDM keystore. This step only applies if you have implemented mutual SSL authentication (server and client certificate authentication); the password sync plugin is configured to use mutual SSL authentication by default.
  3. The encryption key used for the password is correct and present in both the DS/OpenDJ truststore and the IDM/OpenIDM keystore.

IDM/OpenIDM certificate

This process is documented in the Password Synchronization Plugin Guide: To Import the IDM Certificate into the DS Keystore.

You can check that the IDM/OpenIDM certificate has been imported properly as follows:

  1. Use the keytool command to list the certificate aliases in the IDM/OpenIDM keystore to check which certificate is being used; this could be the openidm-localhost certificate or a CA certificate depending on your setup.
  2. Use the keytool command to list the certificate aliases in the DS/OpenDJ truststore and check that the certificate alias returned in step 1 is present. The following example keytool command shows that the openidm-localhost certificate has been imported into the DS/OpenDJ keystore:
    $ keytool -list -keystore truststore -storetype JKS -storepass password -v
    
    Keystore type: JKS
    Keystore provider: SUN
    
    Your keystore contains 2 entries
    
    Alias name: openidm-localhost
    Creation date: 13/07/2016
    Entry type: trustedCertEntry
    
    Owner: CN=localhost, O=OpenIDM Self-Signed Certificate, OU=None, L=None, ST=None, C=None
    Issuer: CN=localhost, O=OpenIDM Self-Signed Certificate, OU=None, L=None, ST=None, C=None
    Serial number: 152f283769d
    Valid from: Tue Jan 19 14:54:07 AEDT 2016 until: Wed Feb 18 14:54:07 AEDT 2026
    Certificate fingerprints:
         MD5:  D4:B4:E7:A5:3C:E9:42:3C:F5:EB:57:5B:5C:D3:18:DC
         SHA1: A6:82:85:49:95:35:71:DF:33:2F:E4:F5:92:AB:B9:65:51:97:2A:D6
         SHA256: D4:55:A9:E8:B6:61:75:39:F4:E7:FE:EE:AE:B0:A2:46:2D:B2:82:81:4A:97:EB:31:BB:8C:E8:35:1E:80:D3:6C
         Signature algorithm name: SHA512withRSA
         Version: 3
    
    
    *******************************************
    *******************************************
    

DS/OpenDJ certificate

This process is documented in the Password Synchronization Plugin Guide: To Import the DS Certificate into the IDM Truststore and is only required for mutual authentication, although the password sync plugin is configured to use mutual SSL authentication by default.

You can check that the DS/OpenDJ certificate has been imported properly as follows:

  1. Use the keytool command to list the certificate aliases in the IDM/OpenIDM keystore and check that the server-cert certificate (or equivalent CA certificate depending on your setup) is being used.
  2. Check that the certificate DN has been added as a value of the allowedAuthenticationIdPatterns property for the CLIENT_CERT authentication module in the authentication.json file (located in the /path/to/idm/conf directory).

Encryption key

The DS/OpenDJ password is encrypted before it is sent to IDM/OpenIDM. The encryption key used is openidm-localhost by default but could be different if you have changed it.

You should check the encryption key as follows:

  1. Check which encryption key is actually being used by DS/OpenDJ using one of the following methods:
    • Look for the value of the ds-cfg-realm property in the config.ldif file (located in the /path/to/ds/config directory).
    • Check the value of the private-key-alias property using the dsconfig command.
    If this is not the key you expected, you should update the ds-cfg-realm property in the config.ldif file with the correct key.
  2. Check that this encryption key is present in both the DS/OpenDJ truststore and the IDM/OpenIDM keystore, otherwise IDM/OpenIDM will not be able to decrypt the password when it is received. If it is not there, import it into the appropriate stores per the documentation links above.

IDM/OpenIDM log

The IDM/OpenIDM log (openidm.log, located in the /path/to/idm/logs directory) can be useful for identifying any issues with the password synchronization process from the IDM/OpenIDM side. You should increase logging in the IDM/OpenIDM debug log to FINEST (set the .level property to FINEST in logging.properties, which is located in the /path/to/idm/conf directory).

SSL and keys

If you see the following message in your IDM/OpenIDM log after changing a password in DS/OpenDJ, it means the SSL connection is working and the request has been received and successfully decrypted:

FINEST: Request: { "content": { "password": { "$crypto": { "value": { "data": "6m3EOH9Fs7gqKNlTphW8Iw==", "cipher": "AES/ECB/PKCS5Padding", "key": { "data": "SKZIzkHc4wSa56sfivwKpblZrN3a/d8H/M10C48VOiaMFwlGM311ISl6s620ysWWXNXyqU4E/+hQFRx4M9pG80aYWWbHZvaicAtXadEs3kPhC8ffpPu8cYmfYbryKzsgQ0CqR+Ke3qERWdLDXRRX+ionkC9CfY4nMMKGhYVXNW61/TtKBrn3g5/3RvmL+mI9IxDeMDTgPsMuqmJtkSwHe2do/WmgeTnMpzMHb3GuXBcbphilzsTtLQvfGFQMC/0WTgwy0tovASIcY0rPRiEm6ymVrYRZTD+Zqy9pTeSVPH+XrqJTJUafIbIQlA1gf54ZnOrB/7Sauyeo7FxS8CzhIQ==", "cipher": "RSA/ECB/OAEPWithSHA1AndMGF1Padding", "key": "openidm-localhost" } }, "type": "x-simple-encryption" } } }, "resourcePath": "policy/managed/user/7a2594ea-ae24-4084-a963-9fa95e1dccc5", "additionalParameters": { "external": "true" }, "action": "validateProperty", "method": "action", "fields": [  ] }

Although the SSL connection is working, you could still see errors if the provided key is unknown to IDM/OpenIDM, for example:

FINEST: Resource exception: 500 Internal Server Error: "Wrapped org.forgerock.json.JsonException: org.forgerock.json.crypto.JsonCryptoException: key not found: openidm-unknown (/path/to/idm/bin/defaults/script/policy.js#690) in /path/to/idm/bin/defaults/script/policy.js at line number 690 at column number 0"

If the SSL connection is not working, you will see a SSL handshake exception, for example:

javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown 
   at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) 
   at sun.security.ssl.Alerts.getSSLException(Alerts.java:154) 
   at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:1959) 
   at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1077) 
   at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312) 
   at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339) 
   at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323) 
Note

If you are experiencing issues with SSL or keys, you can enable SSL debugging as detailed in the SSL debugging section.

Configuration issues

The following message indicates there is an issue with your configuration that is causing a loop whereby the password change originating from DS/OpenDJ results in an update from IDM/OpenIDM back to DS/OpenDJ for the same LDAP entry:

Caused by: org.identityconnectors.framework.common.exceptions.ConnectorException: javax.naming.ServiceUnavailableException: [LDAP: error code 51 - Entry uid=user.1,ou=People,dc=forgerock,dc=com cannot be modified because the server failed to obtain a write lock for this entry after multiple attempts]

To resolve this issue, you must prevent the managed user's password being synced to DS/OpenDJ if the change originated from the DS/OpenDJ password sync plugin. See the DS/OpenDJ password sync flow section above for more details on achieving this. Alternatively, you can change the value of the ds-cfg-update-interval property (in the openidm-pwsync-plugin-config.ldif or openidm-accountchange-plugin-sample-config file) to 1 or 2 seconds; this changes the sync operation to an asynchronous process and may resolve this issue

SSL debugging

SSL debugging traces the SSL handshaking phase. You can enable SSL debugging via the OPENIDM_OPTS environment variable.

On Unix® and Linux® systems:

$ cd /path/to/idm/
$ export OPENIDM_OPTS="-Djavax.net.debug=all"
$ ./startup.sh

On Microsoft® Windows® systems:

C:\> cd \path\to\idm
C:\path\to\idm> set OPENIDM_OPTS=-Djavax.net.debug=all
C:\path\to\idm> startup.bat
Note

You can also edit the startup.sh or startup.bat files to update the default OPENIDM_OPTS values.

DS/OpenDJ configuration files

The following configuration files are useful to check how DS/OpenDJ has been configured and how the password sync plugin has been configured on the DS/OpenDJ side:

  • config.ldif (located in the /path/to/ds/config directory).
  • openidm-pwsync-plugin-config.ldif or openidm-accountchange-plugin-sample-config (located in the /path/to/ds/config directory). The actual file depends on the password sync plugin version.

You can then compare the configuration on the DS/OpenDJ side with the configuration on the IDM/OpenIDM side to ensure they correspond and correct if necessary.

IDM/OpenIDM configuration files

The following configuration files (located in the /path/to/idm/conf directory) are useful to check how synchronization has been configured on the IDM/OpenIDM side and compare with the DS/OpenDJ configuration:

  • sync.json
  • managed.json
  • authentication.json
  • provisioner configuration file for DS/OpenDJ, for example provisioner.openicf-ldap.json.
  • any associated scripts used to manage or transform the password attribute during sync, such as a separate onUpdate script (these are typically located in the /path/to/idm/script directory).
Note

It can be very useful to add logging to your scripts to help identify the point at which an issue can occur. Refer to How do I add logging to JavaScript files in IDM/OpenIDM (All versions)? and How do I add logging to Groovy scripts in IDM/OpenIDM (All versions)? for further information.

DS/OpenDJ logs

The DS/OpenDJ log files (access, errors and replication) can be useful for identifying any issues with the password synchronization process from the DS/OpenDJ side. The logs collected should cover the period during which you are experiencing issues and the timestamps should correspond to the logs collected for IDM/OpenIDM.

Enabling debug logging for the DS/OpenDJ password sync plugin

You can enable debug logging for just the DS/OpenDJ password sync plugin to minimize the size of the resulting DS/OpenDJ logs as follows:

  1. Enable debug logging using the following dsconfig command:
    $ ./dsconfig set-log-publisher-prop --hostname ds1.example.com --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --publisher-name "File-Based Debug Logger" --set enabled:true --no-prompt --trustAll
    
    In OpenDJ 2.6.x, you must include the --set default-debug-level:all option as well.
  2. Set the debug target class using one of the following dsconfig commands depending on your version:
    • OpenDJ 3.5.x / DS:
      $ ./dsconfig create-debug-target --hostname ds1.example.com --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --publisher-name "File-Based Debug Logger" --type generic --target-name org.forgerock.openidm.accountchange.OpenidmAccountStatusNotificationHandler --set enabled:true --no-prompt --trustAll
      
    • OpenDJ 3.0 and earlier:
      $ ./dsconfig create-debug-target --hostname ds1.example.com --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --publisher-name "File-Based Debug Logger" --type generic --target-name org.forgerock.openidm.agent.accountchange.OpenidmAccountStatusNotificationHandler --set enabled:true --no-prompt --trustAll
      

IDM/OpenIDM audit logs

The IDM/OpenIDM audit logs (located in the /path/to/idm/audit directory) can be useful for tracking system activity; the activity.csv file is particularly relevant.

See Also

DS/OpenDJ (All versions) fail to start after upgrade if you use the Password Sync Plugin for IDM/OpenIDM

How do I upgrade DS/OpenDJ (All versions) if I have the DS/OpenDJ Password Sync Plugin for IDM/OpenIDM installed?

OpenDJ Password Synchronization Plugin 1.0.3 install fails on OpenDJ 3.x

How do I add logging to JavaScript files in IDM/OpenIDM (All versions)?

How do I add logging to Groovy scripts in IDM/OpenIDM (All versions)?

Password Synchronization Plugin Guide

Related Training

ForgeRock Identity Management Core Concepts (IDM-400)

Related Issue Tracker IDs

OPENIDM-3824 (Enhance OpenIDM so that only updated fields are synchronized to OpenDJ, rather than the whole managed object)

OPENIDM-3366 (Password sync loop when LDAP groups change)


OpenDJ Password Synchronization Plugin 1.0.3 install fails on OpenDJ 3.x

The purpose of this article is to provide assistance when the OpenDJ Password Synchronization Plugin 1.0.3 install fails on OpenDJ 3.x with an "ADD operation failed Result Code: 53 (Unwilling to Perform)" message. This plugin is available as a separate download for OpenIDM to enable password synchronization between OpenIDM and OpenDJ.

Symptoms

The following message is shown when adding the plugin configuration using the ldapmodify command:

Processing ADD request for cn=OpenIDM Notification Handler,cn=Account Status Notification Handlers,cn=config 
ADD operation failed 
Result Code: 53 (Unwilling to Perform) 
Additional Information: The Directory Server is unwilling to add configuration entry cn=OpenIDM Notification Handler,cn=Account Status Notification Handlers,cn=config because one of the add listeners registered with the parent entry cn=Account Status Notification Handlers,cn=config rejected this change with the message: An error occurred while trying to initialize an instance of class org.forgerock.openidm.agent.accountchange.OpenidmAccountStatusNotificationHandler as an account status notification handler as defined in configuration entry cn=OpenIDM Notification Handler,cn=Account Status Notification Handlers,cn=config: PropertyException: The value "org.forgerock.openidm.agent.accountchange.OpenidmAccountStatusNotificationHandler" is not a valid value for the "java-class" property, which must have the following syntax: CLASS <= org.opends.server.api.AccountStatusNotificationHandler (PropertyException.java:103 ClassPropertyDefinition.java:349 ClassPropertyDefinition.java:329 ClassPropertyDefinition.java:284 AccountStatusNotificationHandlerConfigManager.java:392 AccountStatusNotificationHandlerConfigManager.java:259 AccountStatusNotificationHandlerConfigManager.java:59 ServerManagedObjectAddListenerAdaptor.java:90 ConfigAddListenerAdaptor.java:245 ConfigFileHandler.java:960 LocalBackendAddOperation.java:473 LocalBackendAddOperation.java:173 LocalBackendWorkflowElement.java:737 LocalBackendWorkflowElement.java:1024 LocalBackendWorkflowElement.java:895 AddOperationBasis.java:506 TraditionalWorkerThread.java:158)

Recent Changes

Upgraded to OpenDJ 3.x.

Causes

The OpenDJ Password Synchronization Plugin 1.0.3 is not compatible with OpenDJ 3.x following significant changes to the extension API in OpenDJ 3. This is noted in OpenIDM Release Notes › Before You Install OpenIDM Software › Supported Connectors, Connector Servers, and Plugins.

Solution

This issue can be resolved by upgrading to OpenDJ Password Sync Plugin 1.1.1 if you are using OpenDJ 3.0 or OpenDJ Password Sync Plugin 3.5.0 if you are using OpenDJ 3.5 Enterprise Edition; you can download these from BackStage.

The installation process for this plugin has changed slightly from previous versions as follows:

See Also

OpenIDM Integrator's Guide › Managing Passwords › Password Synchronization

Related Training

N/A

Related Issue Tracker IDs

OPENDJ-2742 (Build IDM account change handler plugin within OpenDJ build)

OPENIDM-4491 (Account Status Notification Handlers configuration fails using OpenDJ-3.0.0)


Copyright and TrademarksCopyright © 2018 - 2019 ForgeRock, all rights reserved.

This content has been optimized for printing.

Loading...