Blob Blame History Raw
From 1441b999d3fe9b4e59fe942294d13480ecee7d94 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy@redhat.com>
Date: Wed, 28 Oct 2020 17:46:56 +0200
Subject: [PATCH] rpcserver: fallback to non-armored kinit in case of trusted
 domains

MIT Kerberos implements FAST negotiation as specified in RFC 6806
section 11. The implementation relies on the caller to provide a hint
whether FAST armoring must be used.

FAST armor can only be used when both client and KDC have a shared
secret. When KDC is from a trusted domain, there is no way to have a
shared secret between a generic Kerberos client and that KDC.

[MS-KILE] section 3.2.5.4 'Using FAST When the Realm Supports FAST'
allows KILE clients (Kerberos clients) to have local settings that
direct it to enforce use of FAST. This is equal to the current
implementation of 'kinit' utility in MIT Kerberos requiring to use FAST
if armor cache (option '-T') is provided.

[MS-KILE] section 3.3.5.7.4 defines a way for a computer from a
different realm to use compound identity TGS-REQ to create FAST TGS-REQ
explicitly armored with the computer's TGT. However, this method is not
available to IPA framework as we don't have access to the IPA server's
host key. In addition, 'kinit' utility does not support this method.

Active Directory has a policy to force use of FAST when client
advertizes its use. Since we cannot know in advance whether a principal
to obtain initial credentials for belongs to our realm or to a trusted
one due to enterprise principal canonicalization, we have to try to
kinit. Right now we fail unconditionally if FAST couldn't be used and
libkrb5 communication with a KDC from the user realm (e.g. from a
trusted forest) causes enforcement of a FAST.

In the latter case, as we cannot use FAST anyway, try to kinit again
without advertizing FAST. This works even in the situations when FAST
enforcement is enabled on Active Directory side: if client doesn't
advertize FAST capability, it is not required. Additionally, FAST cannot
be used for any practical need for a trusted domain's users yet.

Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
 ipalib/errors.py                        |  6 ++
 ipaserver/rpcserver.py                  | 94 ++++++++++++++++---------
 ipatests/test_integration/test_trust.py | 21 ++++++
 3 files changed, 86 insertions(+), 35 deletions(-)

diff --git a/ipalib/errors.py b/ipalib/errors.py
index 1b17ca7ed..fa51e15c0 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -245,6 +245,12 @@ class PluginModuleError(PrivateError):
     format = '%(name)s is not a valid plugin module'
 
 
+class KrbPrincipalWrongFAST(PrivateError):
+    """
+    Raised when it is not possible to use our FAST armor for kinit
+    """
+    format = '%(principal)s cannot use Anonymous PKINIT as a FAST armor'
+
 ##############################################################################
 # Public errors:
 
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 181295471..ed775170e 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -46,9 +46,11 @@ from ipalib.capabilities import VERSION_WITHOUT_CAPABILITIES
 from ipalib.frontend import Local
 from ipalib.install.kinit import kinit_armor, kinit_password
 from ipalib.backend import Executioner
-from ipalib.errors import (PublicError, InternalError, JSONError,
+from ipalib.errors import (
+    PublicError, InternalError, JSONError,
     CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError,
-    ExecutionError, PasswordExpired, KrbPrincipalExpired, UserLocked)
+    ExecutionError, PasswordExpired, KrbPrincipalExpired, KrbPrincipalWrongFAST,
+    UserLocked)
 from ipalib.request import context, destroy_context
 from ipalib.rpc import (xml_dumps, xml_loads,
     json_encode_binary, json_decode_binary)
@@ -957,6 +959,34 @@ class login_password(Backend, KerberosSession):
         self.api.Backend.wsgi_dispatch.mount(self, self.key)
 
     def __call__(self, environ, start_response):
+        def attempt_kinit(user_principal, password,
+                          ipa_ccache_name, use_armor=True):
+            try:
+                # try to remove in case an old file was there
+                os.unlink(ipa_ccache_name)
+            except OSError:
+                pass
+            try:
+                self.kinit(user_principal, password,
+                           ipa_ccache_name, use_armor=use_armor)
+            except PasswordExpired as e:
+                return self.unauthorized(environ, start_response,
+                                         str(e), 'password-expired')
+            except InvalidSessionPassword as e:
+                return self.unauthorized(environ, start_response,
+                                         str(e), 'invalid-password')
+            except KrbPrincipalExpired as e:
+                return self.unauthorized(environ,
+                                         start_response,
+                                         str(e),
+                                         'krbprincipal-expired')
+            except UserLocked as e:
+                return self.unauthorized(environ,
+                                         start_response,
+                                         str(e),
+                                         'user-locked')
+            return None
+
         logger.debug('WSGI login_password.__call__:')
 
         # Get the user and password parameters from the request
@@ -1007,26 +1037,14 @@ class login_password(Backend, KerberosSession):
         ipa_ccache_name = os.path.join(paths.IPA_CCACHES,
                                        'kinit_{}'.format(os.getpid()))
         try:
-            # try to remove in case an old file was there
-            os.unlink(ipa_ccache_name)
-        except OSError:
-            pass
-        try:
-            self.kinit(user_principal, password, ipa_ccache_name)
-        except PasswordExpired as e:
-            return self.unauthorized(environ, start_response, str(e), 'password-expired')
-        except InvalidSessionPassword as e:
-            return self.unauthorized(environ, start_response, str(e), 'invalid-password')
-        except KrbPrincipalExpired as e:
-            return self.unauthorized(environ,
-                                     start_response,
-                                     str(e),
-                                     'krbprincipal-expired')
-        except UserLocked as e:
-            return self.unauthorized(environ,
-                                     start_response,
-                                     str(e),
-                                     'user-locked')
+            result = attempt_kinit(user_principal, password,
+                                   ipa_ccache_name, use_armor=True)
+        except KrbPrincipalWrongFAST:
+            result = attempt_kinit(user_principal, password,
+                                   ipa_ccache_name, use_armor=False)
+
+        if result is not None:
+            return result
 
         result = self.finalize_kerberos_acquisition('login_password',
                                                     ipa_ccache_name, environ,
@@ -1038,21 +1056,24 @@ class login_password(Backend, KerberosSession):
             pass
         return result
 
-    def kinit(self, principal, password, ccache_name):
-        # get anonymous ccache as an armor for FAST to enable OTP auth
-        armor_path = os.path.join(paths.IPA_CCACHES,
-                                  "armor_{}".format(os.getpid()))
+    def kinit(self, principal, password, ccache_name, use_armor=True):
+        if use_armor:
+            # get anonymous ccache as an armor for FAST to enable OTP auth
+            armor_path = os.path.join(paths.IPA_CCACHES,
+                                      "armor_{}".format(os.getpid()))
 
-        logger.debug('Obtaining armor in ccache %s', armor_path)
+            logger.debug('Obtaining armor in ccache %s', armor_path)
 
-        try:
-            kinit_armor(
-                armor_path,
-                pkinit_anchors=[paths.KDC_CERT, paths.KDC_CA_BUNDLE_PEM],
-            )
-        except RuntimeError as e:
-            logger.error("Failed to obtain armor cache")
-            # We try to continue w/o armor, 2FA will be impacted
+            try:
+                kinit_armor(
+                    armor_path,
+                    pkinit_anchors=[paths.KDC_CERT, paths.KDC_CA_BUNDLE_PEM],
+                )
+            except RuntimeError as e:
+                logger.error("Failed to obtain armor cache")
+                # We try to continue w/o armor, 2FA will be impacted
+                armor_path = None
+        else:
             armor_path = None
 
         try:
@@ -1080,6 +1101,9 @@ class login_password(Backend, KerberosSession):
                   'while getting initial credentials') in str(e):
                 raise UserLocked(principal=principal,
                                  message=unicode(e))
+            elif ('kinit: Error constructing AP-REQ armor: '
+                  'Matching credential not found') in str(e):
+                raise KrbPrincipalWrongFAST(principal=principal)
             raise InvalidSessionPassword(principal=principal,
                                          message=unicode(e))
 
diff --git a/ipatests/test_integration/test_trust.py b/ipatests/test_integration/test_trust.py
index a6a055c2a..bec918a31 100644
--- a/ipatests/test_integration/test_trust.py
+++ b/ipatests/test_integration/test_trust.py
@@ -175,6 +175,27 @@ class TestTrust(BaseTestTrust):
         tasks.kdestroy_all(self.master)
         tasks.kinit_admin(self.master)
 
+    def test_password_login_as_aduser(self):
+        """Test if AD user can login with password to Web UI"""
+        ad_admin = 'Administrator@%s' % self.ad_domain
+
+        tasks.kdestroy_all(self.master)
+        user_and_password = ('user=%s&password=%s' %
+                             (ad_admin, self.master.config.ad_admin_password))
+        host = self.master.hostname
+        cmd_args = [
+            paths.BIN_CURL,
+            '-v',
+            '-H', 'referer:https://{}/ipa'.format(host),
+            '-H', 'Content-Type:application/x-www-form-urlencoded',
+            '-H', 'Accept:text/plain',
+            '--cacert', paths.IPA_CA_CRT,
+            '--data', user_and_password,
+            'https://{}/ipa/session/login_password'.format(host)]
+        result = self.master.run_command(cmd_args)
+        assert "Set-Cookie: ipa_session=MagBearerToken" in result.stdout_text
+        tasks.kinit_admin(self.master)
+
     def test_ipauser_authentication_with_nonposix_trust(self):
         ipauser = u'tuser'
         original_passwd = 'Secret123'
-- 
2.29.2