ipa/0017-ipa-pwd-extop-differentiate-OTP-requirements-in-LDAP.patch
Florence Blanc-Renaud 6c2a5fa538 ipa-4.12.1-3
- Resolves: RHEL-49452 Include latest fixes in python3-ipatests packages
- Resolves: RHEL-49433 Adjust "ipa config-mod --addattr ipaconfigstring=EnforceLDAPOTP" to allow for non OTP users in some cases
- Resolves: RHEL-49432 ipa-migrate stage-mode is failing with error: Modifying a mapped attribute in a managed entry is not allowed
- Resolves: RHEL-49413 ipa-migrate with -Z option fails with ValueError: option error
- Resolves: RHEL-47157 ipa-migrate -V options fails to display version
- Resolves: RHEL-47148 Pagure #9629: Syntax error uninstalling the selinux-luna subpackage
- Resolves: RHEL-40892 ipa-server-install: token_password_file read in kra.install_check after calling hsm_validator in ca.install_check

Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
2024-07-18 13:25:00 +02:00

233 lines
11 KiB
Diff

From 051d61fdc301f2768ac78c45e93a5f9eeff8aa28 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy@redhat.com>
Date: Tue, 25 Jun 2024 14:27:24 +0300
Subject: [PATCH] ipa-pwd-extop: differentiate OTP requirements in LDAP binds
For users who has no OTP tokens defined (yet), a missing token should
not be seen as a failure. This is needed to allow a basic password
change.
The logic around enforcement of OTP over LDAP bind is the following:
----------------------------------------------------------------------
- when LDAP OTP control is requested by the LDAP client, OTP is
explicitly required
- when EnforceLDAPOTP is set in the IPA configuration, OTP is implicitly
required, regardless of the state of LDAP client
In either case, only users with 'user-auth-type: otp' are allowed to
authenticate.
If these users have no OTP token associated yet, they will be allowed to
authenticate with their password. This is to allow initial password
change and adding an OTP token.
----------------------------------------------------------------------
Implement test that simulates lifecycle for new user who get to change
their password before adding an OTP token.
Related: https://pagure.io/freeipa/issue/5169
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
.../ipa-slapi-plugins/ipa-pwd-extop/prepost.c | 39 ++++++++++----
ipatests/test_integration/test_otp.py | 52 ++++++++++++++++---
2 files changed, 76 insertions(+), 15 deletions(-)
diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
index cc170fc4b81f8ecad88f4ff4401b5651c43aaf55..c967e2cfffbd920280639f3188783ec150523b47 100644
--- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
+++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
@@ -1212,13 +1212,20 @@ done:
* value at the end. This leaves only the password in creds for later
* validation.
*/
+typedef enum {
+ OTP_IS_NOT_REQUIRED = 0,
+ OTP_IS_REQUIRED_EXPLICITLY,
+ OTP_IS_REQUIRED_IMPLICITLY
+} otp_req_enum;
static bool ipapwd_pre_bind_otp(const char *bind_dn, Slapi_Entry *entry,
- struct berval *creds, bool otpreq)
+ struct berval *creds, otp_req_enum otpreq,
+ bool *notokens)
{
uint32_t auth_types;
/* Get the configured authentication types. */
auth_types = otp_config_auth_types(otp_config, entry);
+ *notokens = false;
/*
* IMPORTANT SECTION!
@@ -1248,7 +1255,11 @@ static bool ipapwd_pre_bind_otp(const char *bind_dn, Slapi_Entry *entry,
/* With no tokens, succeed if tokens aren't required. */
if (tokens[0] == NULL) {
otp_token_free_array(tokens);
- return !otpreq;
+ *notokens = true;
+ if (otpreq != OTP_IS_NOT_REQUIRED)
+ /* DENY: OTP is required, either explicitly or implicitly */
+ return false;
+ return true;
}
if (otp_token_validate_berval(tokens, creds, NULL)) {
@@ -1259,7 +1270,8 @@ static bool ipapwd_pre_bind_otp(const char *bind_dn, Slapi_Entry *entry,
otp_token_free_array(tokens);
}
- return (auth_types & OTP_CONFIG_AUTH_TYPE_PASSWORD) && !otpreq;
+ return (auth_types & OTP_CONFIG_AUTH_TYPE_PASSWORD) &&
+ (otpreq == OTP_IS_NOT_REQUIRED);
}
static int ipapwd_authenticate(const char *dn, Slapi_Entry *entry,
@@ -1452,6 +1464,7 @@ static int ipapwd_pre_bind(Slapi_PBlock *pb)
struct tm expire_tm;
int rc = LDAP_INVALID_CREDENTIALS;
char *errMesg = NULL;
+ bool notokens = false;
/* get BIND parameters */
ret |= slapi_pblock_get(pb, SLAPI_BIND_TARGET_SDN, &target_sdn);
@@ -1510,8 +1523,9 @@ static int ipapwd_pre_bind(Slapi_PBlock *pb)
/* Try to do OTP first. */
syncreq = otpctrl_present(pb, OTP_SYNC_REQUEST_OID);
- otpreq = otpctrl_present(pb, OTP_REQUIRED_OID);
- if (!syncreq && !otpreq) {
+ otpreq = otpctrl_present(pb, OTP_REQUIRED_OID) ?
+ OTP_IS_REQUIRED_EXPLICITLY : OTP_IS_NOT_REQUIRED;
+ if (!syncreq && (otpreq == OTP_IS_NOT_REQUIRED)) {
ret = ipapwd_gen_checks(pb, &errMesg, &krbcfg, IPAPWD_CHECK_ONLY_CONFIG);
if (ret != 0) {
LOG_FATAL("ipapwd_gen_checks failed!?\n");
@@ -1520,11 +1534,17 @@ static int ipapwd_pre_bind(Slapi_PBlock *pb)
return 0;
}
if (krbcfg->enforce_ldap_otp) {
- otpreq = true;
+ otpreq = OTP_IS_REQUIRED_IMPLICITLY;
}
}
- if (!syncreq && !ipapwd_pre_bind_otp(dn, entry, credentials, otpreq))
- goto invalid_creds;
+ if (!syncreq && !ipapwd_pre_bind_otp(dn, entry,
+ credentials, otpreq, &notokens)) {
+ /* We got here because ipapwd_pre_bind_otp() returned false,
+ * it means that either token verification failed or
+ * a rule for empty tokens failed current policy. */
+ if (!(notokens || (otpreq == OTP_IS_NOT_REQUIRED)))
+ goto invalid_creds;
+ }
/* Ensure that there is a password. */
if (credentials->bv_len == 0) {
@@ -1561,7 +1581,8 @@ static int ipapwd_pre_bind(Slapi_PBlock *pb)
* for access log to notice multi-factor authentication has happened
* https://www.port389.org/docs/389ds/design/mfa-operation-note-design.html
*/
- if (!syncreq && otpreq) {
+ if (!syncreq &&
+ ((otpreq != OTP_IS_NOT_REQUIRED) && !notokens)) {
slapi_pblock_set_flag_operation_notes(pb, SLAPI_OP_NOTE_MFA_AUTH);
}
#endif
diff --git a/ipatests/test_integration/test_otp.py b/ipatests/test_integration/test_otp.py
index d2dfca4cbf8c60955e888b6f92bd88a2608bb265..350371bfe1e4c1cc6dcc89f6584f813fcb0d32a0 100644
--- a/ipatests/test_integration/test_otp.py
+++ b/ipatests/test_integration/test_otp.py
@@ -458,41 +458,81 @@ class TestOTPToken(IntegrationTest):
master = self.master
basedn = master.domain.basedn
USER1 = 'user-forced-otp'
+ TMP_PASSWORD = 'Secret1234509'
binddn = DN(f"uid={USER1},cn=users,cn=accounts,{basedn}")
- tasks.create_active_user(master, USER1, PASSWORD)
tasks.kinit_admin(master)
+ master.run_command(['ipa', 'pwpolicy-mod', '--minlife', '0'])
+ tasks.user_add(master, USER1, password=TMP_PASSWORD)
# Enforce use of OTP token for this user
master.run_command(['ipa', 'user-mod', USER1,
'--user-auth-type=otp'])
try:
+ # Change initial password through the IPA endpoint
+ url = f'https://{master.hostname}/ipa/session/change_password'
+ master.run_command(['curl', '-d', f'user={USER1}',
+ '-d', f'old_password={TMP_PASSWORD}',
+ '-d', f'new_password={PASSWORD}',
+ '--referer', f'https://{master.hostname}/ipa',
+ url])
conn = master.ldap_connect()
# First, attempt authenticating with a password but without LDAP
# control to enforce OTP presence and without server-side
# enforcement of the OTP presence check.
conn.simple_bind(binddn, f"{PASSWORD}")
- # Add an OTP token now
- otpuid, totp = add_otptoken(master, USER1, otptype="totp")
# Next, enforce Password+OTP for a user with OTP token
master.run_command(['ipa', 'config-mod', '--addattr',
'ipaconfigstring=EnforceLDAPOTP'])
+ # Try to bind without OTP because there is no OTP token yet,
+ # the operation should succeed because OTP enforcement is implicit
+ # and there is no token yet, so it is allowed.
+ conn.simple_bind(binddn, f"{PASSWORD}")
+ conn.unbind()
+ # Add an OTP token now
+ otpuid, totp = add_otptoken(master, USER1, otptype="totp")
# Next, authenticate with Password+OTP and with the LDAP control
# this operation should succeed
otpvalue = totp.generate(int(time.time())).decode("ascii")
+ conn = master.ldap_connect()
conn.simple_bind(binddn, f"{PASSWORD}{otpvalue}",
client_controls=[
BooleanControl(
controlType="2.16.840.1.113730.3.8.10.7",
booleanValue=True)])
- # Remove token
- del_otptoken(self.master, otpuid)
+ conn.unbind()
+ # Sleep to make sure we are going to use a different token value
+ time.sleep(45)
+ # Use OTP token again, without LDAP control, should succeed
+ # because OTP enforcement is implicit
+ otpvalue = totp.generate(int(time.time())).decode("ascii")
+ conn = master.ldap_connect()
+ conn.simple_bind(binddn, f"{PASSWORD}{otpvalue}")
+ conn.unbind()
# Now, try to authenticate without otp and without control
- # this operation should fail
+ # this operation should fail because we have OTP token associated
+ # with the user account
try:
+ conn = master.ldap_connect()
conn.simple_bind(binddn, f"{PASSWORD}")
+ conn.unbind()
except errors.ACIError:
pass
+ # Sleep to make sure we are going to use a different token value
+ time.sleep(45)
+ # Use OTP token again, without LDAP control, should succeed
+ # because OTP enforcement is implicit
+ otpvalue = totp.generate(int(time.time())).decode("ascii")
+ # Finally, change password again, now that otp is present
+ master.run_command(['curl', '-d', f'user={USER1}',
+ '-d', f'old_password={PASSWORD}',
+ '-d', f'new_password={TMP_PASSWORD}0',
+ '-d', f'otp={otpvalue}',
+ '--referer', f'https://{master.hostname}/ipa',
+ url])
+ # Remove token
+ del_otptoken(self.master, otpuid)
master.run_command(['ipa', 'config-mod', '--delattr',
'ipaconfigstring=EnforceLDAPOTP'])
finally:
+ master.run_command(['ipa', 'pwpolicy-mod', '--minlife', '1'])
master.run_command(['ipa', 'user-del', USER1])
--
2.45.2