diff --git a/API.txt b/API.txt index 7d91077fc..5ed1f5327 100644 --- a/API.txt +++ b/API.txt @@ -1082,7 +1082,7 @@ option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('ca_renewal_master_server?', autofill=False) option: Str('delattr*', cli_name='delattr') option: Flag('enable_sid?', autofill=True, default=False) -option: StrEnum('ipaconfigstring*', autofill=False, cli_name='ipaconfigstring', values=[u'AllowNThash', u'KDC:Disable Last Success', u'KDC:Disable Lockout', u'KDC:Disable Default Preauth for SPNs']) +option: StrEnum('ipaconfigstring*', autofill=False, cli_name='ipaconfigstring', values=[u'AllowNThash', u'KDC:Disable Last Success', u'KDC:Disable Lockout', u'KDC:Disable Default Preauth for SPNs', u'EnforceLDAPOTP']) option: Str('ipadefaultemaildomain?', autofill=False, cli_name='emaildomain') option: Str('ipadefaultloginshell?', autofill=False, cli_name='defaultshell') option: Str('ipadefaultprimarygroup?', autofill=False, cli_name='defaultgroup') diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c index d30764bb2..1355f20d3 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c @@ -83,6 +83,7 @@ static struct ipapwd_krbcfg *ipapwd_getConfig(void) char *tmpstr; int ret; size_t i; + bool fips_enabled = false; config = calloc(1, sizeof(struct ipapwd_krbcfg)); if (!config) { @@ -241,28 +242,35 @@ static struct ipapwd_krbcfg *ipapwd_getConfig(void) config->allow_nt_hash = false; if (ipapwd_fips_enabled()) { LOG("FIPS mode is enabled, NT hashes are not allowed.\n"); + fips_enabled = true; + } + + sdn = slapi_sdn_new_dn_byval(ipa_etc_config_dn); + ret = ipapwd_getEntry(sdn, &config_entry, NULL); + slapi_sdn_free(&sdn); + if (ret != LDAP_SUCCESS) { + LOG_FATAL("No config Entry?\n"); + goto free_and_error; } else { - sdn = slapi_sdn_new_dn_byval(ipa_etc_config_dn); - ret = ipapwd_getEntry(sdn, &config_entry, NULL); - slapi_sdn_free(&sdn); - if (ret != LDAP_SUCCESS) { - LOG_FATAL("No config Entry?\n"); - goto free_and_error; - } else { - tmparray = slapi_entry_attr_get_charray(config_entry, - "ipaConfigString"); - for (i = 0; tmparray && tmparray[i]; i++) { + tmparray = slapi_entry_attr_get_charray(config_entry, + "ipaConfigString"); + for (i = 0; tmparray && tmparray[i]; i++) { + if (strcasecmp(tmparray[i], "EnforceLDAPOTP") == 0) { + config->enforce_ldap_otp = true; + continue; + } + if (!fips_enabled) { if (strcasecmp(tmparray[i], "AllowNThash") == 0) { config->allow_nt_hash = true; continue; } } - if (tmparray) slapi_ch_array_free(tmparray); } - - slapi_entry_free(config_entry); + if (tmparray) slapi_ch_array_free(tmparray); } + slapi_entry_free(config_entry); + return config; free_and_error: @@ -571,6 +579,13 @@ int ipapwd_gen_checks(Slapi_PBlock *pb, char **errMesg, rc = LDAP_OPERATIONS_ERROR; } + /* do not return the master key if asked */ + if (check_flags & IPAPWD_CHECK_ONLY_CONFIG) { + free((*config)->kmkey->contents); + free((*config)->kmkey); + (*config)->kmkey = NULL; + } + done: return rc; } @@ -1103,8 +1118,10 @@ void free_ipapwd_krbcfg(struct ipapwd_krbcfg **cfg) krb5_free_default_realm(c->krbctx, c->realm); krb5_free_context(c->krbctx); - free(c->kmkey->contents); - free(c->kmkey); + if (c->kmkey) { + free(c->kmkey->contents); + free(c->kmkey); + } free(c->supp_encsalts); free(c->pref_encsalts); slapi_ch_array_free(c->passsync_mgrs); diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h index 79606a8c7..976970006 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h @@ -70,6 +70,7 @@ #define IPAPWD_CHECK_CONN_SECURE 0x00000001 #define IPAPWD_CHECK_DN 0x00000002 +#define IPAPWD_CHECK_ONLY_CONFIG 0x00000004 #define IPA_CHANGETYPE_NORMAL 0 #define IPA_CHANGETYPE_ADMIN 1 @@ -109,6 +110,7 @@ struct ipapwd_krbcfg { char **passsync_mgrs; int num_passsync_mgrs; bool allow_nt_hash; + bool enforce_ldap_otp; }; int ipapwd_entry_checks(Slapi_PBlock *pb, struct slapi_entry *e, diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c index 6898e6596..690235150 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c @@ -1431,6 +1431,7 @@ static int ipapwd_pre_bind(Slapi_PBlock *pb) "krbPasswordExpiration", "krblastpwchange", NULL }; + struct ipapwd_krbcfg *krbcfg = NULL; struct berval *credentials = NULL; Slapi_Entry *entry = NULL; Slapi_DN *target_sdn = NULL; @@ -1505,6 +1506,18 @@ 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) { + ret = ipapwd_gen_checks(pb, &errMesg, &krbcfg, IPAPWD_CHECK_ONLY_CONFIG); + if (ret != 0) { + LOG_FATAL("ipapwd_gen_checks failed!?\n"); + slapi_entry_free(entry); + slapi_sdn_free(&sdn); + return 0; + } + if (krbcfg->enforce_ldap_otp) { + otpreq = true; + } + } if (!syncreq && !ipapwd_pre_bind_otp(dn, entry, credentials, otpreq)) goto invalid_creds; @@ -1543,6 +1556,7 @@ static int ipapwd_pre_bind(Slapi_PBlock *pb) return 0; invalid_creds: + free_ipapwd_krbcfg(&krbcfg); slapi_entry_free(entry); slapi_sdn_free(&sdn); slapi_send_ldap_result(pb, rc, NULL, errMesg, 0, NULL); diff --git a/doc/api/config_mod.md b/doc/api/config_mod.md index c479a0344..b3203c350 100644 --- a/doc/api/config_mod.md +++ b/doc/api/config_mod.md @@ -27,7 +27,7 @@ No arguments. * ipauserobjectclasses : :ref:`Str` * ipapwdexpadvnotify : :ref:`Int` * ipaconfigstring : :ref:`StrEnum` - * Values: ('AllowNThash', 'KDC:Disable Last Success', 'KDC:Disable Lockout', 'KDC:Disable Default Preauth for SPNs') + * Values: ('AllowNThash', 'KDC:Disable Last Success', 'KDC:Disable Lockout', 'KDC:Disable Default Preauth for SPNs', 'EnforceLDAPOTP') * ipaselinuxusermaporder : :ref:`Str` * ipaselinuxusermapdefault : :ref:`Str` * ipakrbauthzdata : :ref:`StrEnum` diff --git a/ipaserver/plugins/config.py b/ipaserver/plugins/config.py index eface545d..45bd0c108 100644 --- a/ipaserver/plugins/config.py +++ b/ipaserver/plugins/config.py @@ -247,7 +247,8 @@ class config(LDAPObject): doc=_('Extra hashes to generate in password plug-in'), values=(u'AllowNThash', u'KDC:Disable Last Success', u'KDC:Disable Lockout', - u'KDC:Disable Default Preauth for SPNs'), + u'KDC:Disable Default Preauth for SPNs', + u'EnforceLDAPOTP'), ), Str('ipaselinuxusermaporder', label=_('SELinux user map order'), diff --git a/ipatests/test_integration/test_otp.py b/ipatests/test_integration/test_otp.py index 8e2ea563f..d2dfca4cb 100644 --- a/ipatests/test_integration/test_otp.py +++ b/ipatests/test_integration/test_otp.py @@ -21,6 +21,9 @@ from ipaplatform.paths import paths from ipatests.pytest_ipa.integration import tasks from ipapython.dn import DN +from ldap.controls.simple import BooleanControl + +from ipalib import errors PASSWORD = "DummyPassword123" USER = "opttestuser" @@ -450,3 +453,46 @@ class TestOTPToken(IntegrationTest): assert "ipa-otpd" not in failed_services.stdout_text finally: del_otptoken(self.master, otpuid) + + def test_totp_ldap(self): + master = self.master + basedn = master.domain.basedn + USER1 = 'user-forced-otp' + binddn = DN(f"uid={USER1},cn=users,cn=accounts,{basedn}") + + tasks.create_active_user(master, USER1, PASSWORD) + tasks.kinit_admin(master) + # Enforce use of OTP token for this user + master.run_command(['ipa', 'user-mod', USER1, + '--user-auth-type=otp']) + try: + 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']) + # Next, authenticate with Password+OTP and with the LDAP control + # this operation should succeed + otpvalue = totp.generate(int(time.time())).decode("ascii") + 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) + # Now, try to authenticate without otp and without control + # this operation should fail + try: + conn.simple_bind(binddn, f"{PASSWORD}") + except errors.ACIError: + pass + master.run_command(['ipa', 'config-mod', '--delattr', + 'ipaconfigstring=EnforceLDAPOTP']) + finally: + master.run_command(['ipa', 'user-del', USER1])