diff --git a/src/tests/intg/test_pam_responder.py b/src/tests/intg/test_pam_responder.py index cb864523958..9231e34c0c3 100644 --- a/src/tests/intg/test_pam_responder.py +++ b/src/tests/intg/test_pam_responder.py @@ -29,15 +29,12 @@ import time import config -import intg.ds_openldap import kdc import pytest from intg.util import unindent -LDAP_BASE_DN = "dc=example,dc=com" - def provider_list(): # The comma is required to indicate a list with the string 'proxy' as # only item, without it the string 'proxy' will be interpreted as list @@ -57,71 +54,6 @@ def __init__(self, p): self.p = none -@pytest.fixture(scope="module") -def ad_inst(request): - """Fake AD server instance fixture""" - instance = intg.ds_openldap.FakeAD( - config.PREFIX, 10389, LDAP_BASE_DN, - "cn=admin", "Secret123" - ) - - try: - instance.setup() - except Exception: - instance.teardown() - raise - request.addfinalizer(instance.teardown) - return instance - - -@pytest.fixture(scope="module") -def ldap_conn(request, ad_inst): - """LDAP server connection fixture""" - ldap_conn = ad_inst.bind() - ldap_conn.ad_inst = ad_inst - request.addfinalizer(ldap_conn.unbind_s) - return ldap_conn - - -def format_basic_conf(ldap_conn): - """Format a basic SSSD configuration""" - return unindent("""\ - [sssd] - domains = FakeAD - services = pam, nss - - [nss] - - [pam] - debug_level = 10 - - [domain/FakeAD] - debug_level = 10 - ldap_search_base = {ldap_conn.ad_inst.base_dn} - ldap_referrals = false - - id_provider = ldap - auth_provider = ldap - chpass_provider = ldap - access_provider = ldap - - ldap_uri = {ldap_conn.ad_inst.ldap_url} - ldap_default_bind_dn = {ldap_conn.ad_inst.admin_dn} - ldap_default_authtok_type = password - ldap_default_authtok = {ldap_conn.ad_inst.admin_pw} - - ldap_schema = ad - ldap_id_mapping = true - ldap_idmap_default_domain_sid = S-1-5-21-1305200397-2901131868-73388776 - case_sensitive = False - - [prompting/password] - password_prompt = My global prompt - - [prompting/password/pam_sss_alt_service] - password_prompt = My alt service prompt - """).format(**locals()) - USER1 = dict(name='user1', passwd='x', uid=10001, gid=20001, gecos='User for tests', dir='/home/user1', @@ -198,31 +130,6 @@ def format_pam_cert_auth_conf_name_format(config, provider): """).format(**locals()) -def format_pam_krb5_auth(config, kdc_instance): - """Format SSSD configuration for krb5 authentication""" - return unindent("""\ - [sssd] - debug_level = 10 - domains = krb5_auth - services = pam, nss - - [nss] - debug_level = 10 - - [pam] - debug_level = 10 - - [domain/krb5_auth] - debug_level = 10 - id_provider = proxy - proxy_lib_name = call - auth_provider = krb5 - - krb5_realm = PAMKRB5TEST - krb5_server = localhost:{kdc_instance.kdc_port} - """).format(**locals()) - - def format_pam_krb5_auth_domains(config, kdc_instance): """Format SSSD configuration for krb5 authentication""" return unindent("""\ @@ -356,26 +263,6 @@ def simple_pam_cert_auth(request, passwd_ops_setup): return None -@pytest.fixture -def simple_pam_cert_auth_no_cert(request, passwd_ops_setup): - """Setup SSSD with pam_cert_auth=True""" - config.PAM_CERT_DB_PATH = os.environ['PAM_CERT_DB_PATH'] - - old_softhsm2_conf = os.environ['SOFTHSM2_CONF'] - del os.environ['SOFTHSM2_CONF'] - - conf = format_pam_cert_auth_conf(config, provider_switch(request.param)) - create_conf_fixture(request, conf) - create_sssd_fixture(request) - - os.environ['SOFTHSM2_CONF'] = old_softhsm2_conf - - passwd_ops_setup.useradd(**USER1) - passwd_ops_setup.useradd(**USER2) - - return None - - @pytest.fixture def simple_pam_cert_auth_two_certs(request, passwd_ops_setup): """Setup SSSD with pam_cert_auth=True""" @@ -409,73 +296,6 @@ def simple_pam_cert_auth_name_format(request, passwd_ops_setup): return None -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_preauth_indicator(simple_pam_cert_auth): - """Check if preauth indicator file is created""" - statinfo = os.stat(config.PUBCONF_PATH + "/pam_preauth_available") - assert stat.S_ISREG(statinfo.st_mode) - - -@pytest.fixture -def pam_prompting_config(request, ldap_conn): - """Setup SSSD with PAM prompting config""" - conf = format_basic_conf(ldap_conn) - create_conf_fixture(request, conf) - create_sssd_fixture(request) - return None - - -def test_password_prompting_config_global(ldap_conn, pam_prompting_config, - env_for_sssctl): - """Check global change of the password prompt""" - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1_dom1-19661", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="111") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("My global prompt") != -1 - - -def test_password_prompting_config_srv(ldap_conn, pam_prompting_config, - env_for_sssctl): - """Check change of the password prompt for dedicated service""" - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1_dom1-19661", - "--action=auth", - "--service=pam_sss_alt_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="111") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("My alt service prompt") != -1 - - @pytest.fixture def env_for_sssctl(request): pwrap_runtimedir = os.getenv("PAM_WRAPPER_SERVICE_DIR") @@ -491,79 +311,6 @@ def env_for_sssctl(request): return env_for_sssctl -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_sc_auth_wrong_pin(simple_pam_cert_auth, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="111") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: " - "Authentication failure") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_sc_auth(simple_pam_cert_auth, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: Success") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth_two_certs', provider_list(), indirect=True) -def test_sc_auth_two(simple_pam_cert_auth_two_certs, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="2\n123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: Success") != -1 - - @pytest.mark.parametrize('simple_pam_cert_auth_two_certs', provider_list(), indirect=True) def test_sc_auth_two_missing_name(simple_pam_cert_auth_two_certs, env_for_sssctl): @@ -588,170 +335,6 @@ def test_sc_auth_two_missing_name(simple_pam_cert_auth_two_certs, env_for_sssctl assert err.find("pam_authenticate for user [user1]: Success") != -1 -@pytest.mark.parametrize('simple_pam_cert_auth', ['proxy_password'], indirect=True) -def test_sc_proxy_password_fallback(simple_pam_cert_auth, env_for_sssctl): - """ - Check that there will be a password prompt if another proxy auth module is - configured and Smartcard authentication is not allowed but a Smartcard is - present. - """ - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - assert err.find("Password:") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth', ['proxy_password_with_sc'], - indirect=True) -def test_sc_proxy_no_password_fallback(simple_pam_cert_auth, env_for_sssctl): - """ - Use the same environ as for test_sc_proxy_password_fallback but now allow - local Smartcard authentication. Here we expect that there will be a prompt - for the Smartcard PIN and that Smartcard authentication is successful. - """ - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: Success") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_require_sc_auth(simple_pam_cert_auth, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", - "--service=pam_sss_sc_required"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: Success") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth_no_cert', provider_list(), indirect=True) -def test_require_sc_auth_no_cert(simple_pam_cert_auth_no_cert, env_for_sssctl): - - # We have to wait about 20s before the command returns because there will - # be 2 run since retry=1 in the PAM configuration and both - # p11_child_timeout and p11_wait_for_card_timeout are 5s in sssd.conf, - # so 2*(5+5)=20. */ - start_time = time.time() - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", - "--service=pam_sss_sc_required"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - end_time = time.time() - assert end_time > start_time and \ - (end_time - start_time) >= 20 and \ - (end_time - start_time) < 40 - assert out.find("Please insert smart card\nPlease insert smart card") != -1 - assert err.find("pam_authenticate for user [user1]: Authentication " - "service cannot retrieve authentication info") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_try_sc_auth_no_map(simple_pam_cert_auth, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user2", - "--action=auth", - "--service=pam_sss_try_sc"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user2]: Authentication " - "service cannot retrieve authentication info") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_try_sc_auth(simple_pam_cert_auth, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", - "--service=pam_sss_try_sc"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: Success") != -1 - - @pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) def test_try_sc_auth_root(simple_pam_cert_auth, env_for_sssctl): """ @@ -881,73 +464,6 @@ def kdc_instance(request): return kdc_instance -@pytest.fixture -def setup_krb5(request, kdc_instance, passwd_ops_setup): - """ - Setup SSSD for Kerberos authentication with 2 users with different - passwords - """ - conf = format_pam_krb5_auth(config, kdc_instance) - create_conf_fixture(request, conf) - create_sssd_fixture(request, kdc_instance.krb5_conf_path) - - passwd_ops_setup.useradd(**USER1) - passwd_ops_setup.useradd(**USER2) - kdc_instance.add_principal("user1", "Secret123User1") - kdc_instance.add_principal("user2", "Secret123User2") - time.sleep(2) # Give KDC time to initialize - return None - - -def test_krb5_auth(setup_krb5, env_for_sssctl): - """ - Test basic Kerberos authentication, check for authentication failure when - a wrong password is used - """ - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", - "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="Secret123User1") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find(r"pam_authenticate for user [user1]: Success") != -1 - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user2", - "--action=auth", - "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="Secret123User1") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find(r"pam_authenticate for user [user2]: " - "Authentication failure") != -1 - - @pytest.fixture def setup_krb5_domains(request, kdc_instance, passwd_ops_setup): """ diff --git a/src/tests/system/tests/test_authentication.py b/src/tests/system/tests/test_authentication.py index 277f1454184..0e132876921 100644 --- a/src/tests/system/tests/test_authentication.py +++ b/src/tests/system/tests/test_authentication.py @@ -375,3 +375,37 @@ def test_ensure_localauth_plugin_is_not_configured(client: Client, provider: Gen with pytest.raises(Exception): client.fs.read("/var/lib/sss/pubconf/krb5.include.d/localauth_plugin") + + +@pytest.mark.importance("medium") +@pytest.mark.topology(KnownTopologyGroup.AnyProvider) +@pytest.mark.parametrize( + "prompting_section", + ["prompting/password", "prompting/password/su-l"], + ids=["global_prompt", "service_prompt"], +) +def test_authentication__custom_password_prompt_is_shown_at_login( + client: Client, provider: GenericProvider, prompting_section: str +): + """ + :title: Custom password prompt text is shown at login + :description: + 'su -' uses the 'su-l' PAM service, so the per-service case targets + '[prompting/password/su-l]', not '[prompting/password/su]'. + :setup: + 1. Create user + 2. Set a custom 'password_prompt', either globally or for the 'su -' PAM service ('su-l') + 3. Start SSSD + :steps: + 1. Authenticate as the user via 'su -' + :expectedresults: + 1. The custom prompt text is shown and authentication succeeds + :customerscenario: True + """ + provider.user("user1").add(password="Secret123") + client.sssd.section(prompting_section)["password_prompt"] = "My custom prompt" + client.sssd.start() + + result = client.host.conn.run("su - user1 -c 'su - user1 -c whoami'", input="Secret123") + assert "My custom prompt" in result.stderr, "Custom password prompt was not shown!" + assert "user1" in result.stdout, "'user1' failed to log in!" diff --git a/src/tests/system/tests/test_smartcard.py b/src/tests/system/tests/test_smartcard.py index e455371aa47..6645cff8c88 100644 --- a/src/tests/system/tests/test_smartcard.py +++ b/src/tests/system/tests/test_smartcard.py @@ -309,3 +309,136 @@ def test_smartcard__without_soft_ocsp_with_unreachable_responder(client: Client, assert ( "PIN" not in result.stderr or result.rc != 0 ), f"Expected authentication to fail without soft_ocsp when OCSP is unreachable! rc={result.rc}" + + +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__login_fails_when_wrong_pin_is_entered(client: Client): + """ + :title: Smartcard login fails when the wrong pin is entered. + :setup: + 1. Create a local user and initialize a smart card mapped to the user + :steps: + 1. Authenticate as the user via 'su' with an incorrect PIN + :expectedresults: + 1. Authentication fails + :customerscenario: True + """ + client.local.user("user1").add() + client.smartcard.setup_local_card(client, "user1") + + assert not client.auth.su.smartcard("user1", "000000"), "Authentication should have failed with a wrong PIN!" + + +@pytest.mark.importance("medium") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__login_fails_when_card_is_not_mapped(client: Client): + """ + :title: Smartcard authentication fails when card is not mapped to the user + :setup: + 1. Create two local users and initialize a smart card mapped to only the first user + :steps: + 1. Authenticate as the first user via 'su' with the smart card PIN + 2. Attempt to authenticate as the second user via 'su' with the same smart card PIN + :expectedresults: + 1. Authentication succeeds using the certificate + 2. Authentication fails because the certificate does not map to the second user + :customerscenario: True + """ + client.local.user("user1").add() + client.local.user("user2").add() + client.smartcard.setup_local_card(client, "user1") + + assert client.auth.su.smartcard("user1", TOKEN_PIN), "Smart card authentication failed for the mapped user!" + assert not client.auth.su.smartcard( + "user2", TOKEN_PIN + ), "Authentication should fail for a user the certificate does not map to!" + + +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.parametrize( + "pam_p11_allowed_services, expect_cert_auth", + [(None, True), ("-su-l", False)], + ids=["su_l_allowed_by_default", "su_l_removed_from_allowed_services"], +) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__certificate_authentication_is_limited_to_allowed_pam_services( + client: Client, pam_p11_allowed_services: str | None, expect_cert_auth: bool +): + """ + :title: Smartcard authentication is only used for PAM services allowed by pam_p11_allowed_services + :setup: + 1. Optionally remove the 'su-l' service (used by ``su -``) from 'pam_p11_allowed_services' + 2. Create a local user and initialize a smart card mapped to the user + :steps: + 1. Authenticate as the user via 'su -' presenting the smart card PIN + :expectedresults: + 1. Authentication uses the certificate when 'su-l' is an allowed service; when it is not, + 'su -' does not prompt for a PIN and the PIN is rejected as a regular password + :customerscenario: True + """ + client.local.user("user1").add() + if pam_p11_allowed_services is not None: + client.sssd.pam["pam_p11_allowed_services"] = pam_p11_allowed_services + client.smartcard.setup_local_card(client, "user1") + + result = client.auth.su.smartcard_with_output("user1", TOKEN_PIN) + if expect_cert_auth: + assert result.rc == 0, "Smart card authentication should have succeeded!" + assert "PIN" in result.stderr, "'su -' should have prompted for a PIN!" + else: + assert "PIN" not in result.stderr, "'su -' should not prompt for a PIN when it is not an allowed service!" + assert result.rc != 0, f"'{TOKEN_PIN}' should not be accepted as user1's login password!" + + +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__login_succeeds_when_cert_auth_required(client: Client): + """ + :title: Smartcard login succeeds when certificate authentication is required + :setup: + 1. Create a local user and initialize a smart card mapped to the user + 2. Require certificate-based authentication (authselect 'with-smartcard-required') + :steps: + 1. Authenticate as the user via 'su' with the smart card PIN + :expectedresults: + 1. Authentication succeeds + :customerscenario: True + """ + client.local.user("user1").add() + client.smartcard.setup_local_card(client, "user1") + client.authselect.select("sssd", ["with-smartcard-required"]) + + assert client.auth.su.smartcard("user1", TOKEN_PIN), "Smart card authentication failed!" + + +@pytest.mark.importance("medium") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__login_fails_when_cert_auth_required_without_card(client: Client): + """ + :title: Smartcard login fails when certificate authentication is required and no card is present + :setup: + 1. Create a local user + 2. Reduce the smart card wait timeouts + 3. Initialize a smart card mapped to the user and require certificate-based + authentication (authselect 'with-smartcard-required') + 4. Remove the smart card + :steps: + 1. Attempt to authenticate as the user via 'su' + :expectedresults: + 1. Authentication fails because no smart card was inserted before the timeout + :customerscenario: True + """ + client.local.user("user1").add() + client.sssd.pam["p11_child_timeout"] = "1" + client.sssd.pam["p11_wait_for_card_timeout"] = "1" + client.smartcard.setup_local_card(client, "user1") + client.authselect.select("sssd", ["with-smartcard-required"]) + client.smartcard.remove_card() + + assert not client.auth.su.smartcard("user1", TOKEN_PIN), "Authentication should have failed without a card!"