-
Notifications
You must be signed in to change notification settings - Fork 8k
Implement forced password change for LDAP federated user (password policy control) #15253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
f5584dd to
06c23fe
Compare
e588628 to
fee8b17
Compare
|
If I understood correctly, LDAP map storage implementation is still not hooked into authentication, so ppolicy support cannot be copied fully to there at this point. |
|
Just a friendly ping: this PR is ready for review! @mposolda would you have time to take a look, or other maintainers? |
|
Pinging again. Any comments greatly appreciated! |
565b90a to
2fc6b4b
Compare
|
🚀 🚀 🚀 This PR seems to be very useful and we exactly needs the feature for LDAP to pass the password expiry info to keycloak! |
68b3150 to
bdf043c
Compare
ghost
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unreported flaky test detected, please review
Unreported flaky test detectedIf the below flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR. org.keycloak.testsuite.x509.X509BrowserCRLTest#loginSuccessWithCRLSignedWithIntermediateCA3FromTruststoreKeycloak CI - FIPS IT (non-strict) org.keycloak.testsuite.x509.X509BrowserCRLTest#loginFailedWithIntermediateRevocationListFromFileKeycloak CI - FIPS IT (non-strict) org.keycloak.testsuite.x509.X509BrowserCRLTest#loginFailedWithIntermediateRevocationListFromHttpKeycloak CI - FIPS IT (non-strict) org.keycloak.testsuite.x509.X509BrowserCRLTest#loginFailedWithInvalidSignatureCRLKeycloak CI - FIPS IT (non-strict) |
|
I've updated the PR to catch up the latest changes, including the removal of map-store. Also wanted to say that I'd still be grateful to receive reviews, and happy to implement any requests to help getting the feature included! |
pedroigor
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tsaarni Sorry for the delay. It looks like a great addition.
I would consider adding a (yet another) switch to enable the behavior you are introducing given how sensitive it is to support things across different vendors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate why we are removing this line? There is an expectation that this should fail if Start TLS is not possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will need to check this again and come back to this
(This was long time ago but I vaguely remember something like this: after adding the LDAP control, it triggered different behavior from Java LDAP implementation and the dummy LDAP search was not required anymore to send StartTLS. Regardless what it was, when we add on/off switch for ppolicy control, this needs to be revisited anyways)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. If this is not breaking starttls, works for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've now re-organized the code in a way that startTLS() method is purely for issuing start TLS extended operation and it has no "side effect" of triggering authentication, like the extra LDAP search ldapContext.lookup() did before. For authentication I've added explicit ldapContext.reconnect().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about removing the lookup, as it was added here #6592 I think for security reasons. I'll check that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was originally added for security reasons. Coincidentally, it was something I discovered at the time and even got credited for my only "CVE worthy" finding so far 🎉 Also test cases were added back then to cover this exact scenario and they still are there today. Back in 2019, I worked on this bug together with @mposolda and another person who no longer seems to be involved with Keycloak or work at IBM / Red Hat, so I’ll leave him unmentioned, not to bother him 😀
In the current main, the ldapContext.lookup() call is the very first LDAP operation executed after connecting. Due to the way JDK’s LDAP implementation works, the first operation triggers an LDAP bind to authenticate the connection. The lookup / search is empty, but it is used for its side-effect. The bug back then was that ldapContext.lookup() was wrapped in a try ... catch, which ended up ignoring the exception that authentication depended on. The fix in #6592 was that lookup() was moved outside the try ... catch so that authentication errors would no longer be swallowed inside the startTLS() method.
In this PR, I’ve restructured the code so that startTLS() is strictly responsible for issuing the StartTLS extended operation but not sending LDAP bind yet. For authentication, I’ve added an explicit ldapContext.reconnect() call, which triggers the LDAP bind in a single place regardless of StartTLS being used for the connection or not. The underlying reason for this re-organization was that reconnect() allows adding controls to the LDAP bind, also when StartTLS is used, while lookup() does not allow the same for the "side-effect bind" that it triggers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of history here, and glad we have people like you helping us during this time :)
Thanks for the details.
...ain/java/org/keycloak/storage/ldap/idm/store/ldap/control/PasswordPolicyResponseControl.java
Outdated
Show resolved
Hide resolved
Great, I will refresh this PR ASAP! I'll add switch to enable the LDAP control. Would it default to |
bdf043c to
3a5a80a
Compare
|
@sguilhen Can we have a third pair of eyes on this one? |
|
|
||
| @Override | ||
| public void close() { | ||
| if (vaultStringSecret != null) vaultStringSecret.close(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see calling close is basically setting the secret to null but I'm not sure if we want to remove calling it for the sake of respecting the expectations around vault secrets where we might want to clean-up resources for whatever reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm suggesting not having vaultStringSecret as a member variable at all, therefore removal of close(). Though, in case of vaultStringSecret.close() was overwritten as an empty method, so I think it had no effect.
I'm suggesting removal of vaultStringSecret member since Optional complicated finding the bind password unnecessarily in several places. I'm suggesting having new method that handles that in single place and has the vault secret as a local variable:
// Get bind password from vault or from directly from configuration, may be null.
private String getBindPassword() {
VaultStringSecret vaultSecret = session.vault().getStringSecret(ldapConfig.getBindCredential());
return vaultSecret.get().orElse(ldapConfig.getBindCredential());
}
I'm having some failed tests that I've yet to understand, and therefore set this as Draft. But reviews are of course appreciated! 🙏 |
|
@pedroigor can you re-run the failing test group? I don't think the failures are related to this PR at all |
3fcd984 to
c1f364a
Compare
|
I've marked the PR as ready-for-review and updated the description to reflect the current state with the UI changes. I've split the PR into two commits for clarity:
|
|
The failing test needs to be checked - not sure how exactly it is related to this PR, but the failure has been consistent here. |
I found that the error can be reproduced locally only when running testsuite with The test case instantiates Line 346 in c1f364a
If I understood it correctly, the code serializes the object and runs it in context of the server process, and it now fails like this In this PR I've used keycloak/crypto/elytron/pom.xml Lines 54 to 57 in 5ad3aba
federation/ldap/pom.xml where it was not used before. Wildfly-elytron itself depends on jackson-databind (link)
I guess this now causes a conflict: I'm not sure how to solve this. Keycloak itself |
|
@pedroigor any ideas about this test? |
ssilvert
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving the UI part.
|
To fix #15253 (comment) I added hand-written ASN1 BER decoding to replace dependency to wildfly-elytron decoder 91826d1. This is bit similar approach previously taken here in PasswordModifyRequest.java though it is encoding, not decoding, and it did not attempt to add (base for) "generic" implementation. |
8923e58 to
91826d1
Compare
30d7138 to
2498510
Compare
|
I’ve rebased and fixed this PR so it’s up to date with |
2498510 to
679a98c
Compare
Yeah, it should default to |
679a98c to
fe68dc8
Compare
|
I’ve rebased the PR to the latest main branch. I’d appreciate your review and any ideas on how to get this moving again. Thank You! |
fe68dc8 to
6910f95
Compare
This commit contains following changes 1. Refactored StartTLS with LDAP. Refactor initialization of LDAP context into explicit separate steps: a. Initialize context with LDAP connection properties only b. Optionally send StartTLS extended request c. Send bind Previously both connection and authentication properties were set together which triggers implicit authentication in the JDK LDAP and it happened differently depending if StartTLS was used or not. This change moves bind into explicit context.reconnect() call, which also makes is possible in future to send LDAP control messages with bind. That is not possible with implicit bind. 2. Add limited support for LDAP password policy control Send password policy control during LDAP bind operation and receive response according to chapter 9.1 of [1]. When server responds with error "changeAfterReset" then prompt user to update their password by adding UPDATE_PASSWORD required action Following preconditions need to be met: - import mode enabled, otherwise required actions cannot be persisted. - edit mode is set writable, so that users can modify their passwords. Without these preconditions changeAfterReset is treated as failed authentication. 3. Implement ASN1 decoding for ppolicy control [1] https://datatracker.ietf.org/doc/html/draft-behera-ldap-password-policy-11 Signed-off-by: Tero Saarni <[email protected]>
6910f95 to
4ca41fb
Compare
This change adds limited support for LDAP password policy control. The support is limited to prompting user to update their password when the LDAP server indicates that password must be changed. Previously Keycloak let the user in and ignored the mandatory password reset.
After applying this PR there is new optional setting "Enable LDAP password policy" in LDAP advanced settings. It defaults to
false. When set totruethePasswordPolicyRequestcontrol is sent during authentication as part of LDAPbindoperation, when it is executed on user's behalf (but not for admin connection). The request is marked withcriticality=FALSEwhich means that the server can ignore it if it does not support the LDAP control. If the server processes the control, it may respond withPasswordPolicyResponsewitherrorcode valuechangeAfterResetwhich is defined by the specification as followingWith this the LDAP server indicates that the user must update their password. In this case
UPDATE_PASSWORDrequired action is added for the user to trigger existing update password page. The approach follows similar implementation for MSAD.Following limitations apply:
pwdMustChange: TRUEis set and an administrator updates a user’s password, the server will setpwdReset: TRUEfor that user, forcing another password change. This can cause an infinite reset loop. In current OpenLDAP versions, this behaviour occurs when the administrator has themanageACL privilege on the password attribute. To prevent it, grant Keycloak’s LDAP account only thewriteprivilege (notmanage).Forced password change is common use case in many organizations. Examples of LDAP servers that are known to support the Password Policy extension:
Other uses of this control are beyond the scope of this PR. For example, in OpenLDAP, a custom password check module can enforce complexity rules during password changes, but only if the control is included in the password update request.
Fixes #14523