6c2a5fa538
- 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>
233 lines
11 KiB
Diff
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, ¬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
|
|
|