From 051d61fdc301f2768ac78c45e93a5f9eeff8aa28 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy 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 Reviewed-By: Florence Blanc-Renaud Reviewed-By: Rob Crittenden --- .../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, ¬okens)) { + /* 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