diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py
index b9e32369b594c9d32bb1d5dccc453cec495abbfc..6485ec8ff0960637c4016ea8c736f41d8acbd47a 100644
--- a/plugins/module_utils/ansible_freeipa_module.py
+++ b/plugins/module_utils/ansible_freeipa_module.py
@@ -86,6 +86,7 @@ else:
     from ipaplatform.paths import paths
     from ipalib.krb_utils import get_credentials_if_valid
     from ipapython.dnsutil import DNSName
+    from ipapython import kerberos
     from ansible.module_utils.basic import AnsibleModule
     from ansible.module_utils._text import to_text
     from ansible.module_utils.common.text.converters import jsonify
@@ -550,6 +551,87 @@ else:
             return False
         return True
 
+    def servicedelegation_normalize_principals(module, principal):
+        """
+        Normalize servicedelegation principals.
+
+        The principals can be service and with IPA 4.9.0+ also host principals.
+        """
+
+        def _normalize_principal_name(name, realm):
+            # Normalize principal name
+            # Copied from ipaserver/plugins/servicedelegation.py
+            try:
+                princ = kerberos.Principal(name, realm=realm)
+            except ValueError as _err:
+                raise ipalib_errors.ValidationError(
+                    name='principal',
+                    reason="Malformed principal: %s" % str(_err))
+
+            if len(princ.components) == 1 and \
+               not princ.components[0].endswith('$'):
+                nprinc = 'host/' + unicode(princ)
+            else:
+                nprinc = unicode(princ)
+            return nprinc
+
+        def _check_exists(module, _type, name):
+            # Check if item of type _type exists using the show command
+            try:
+                module.ipa_command("%s_show" % _type, name, {})
+            except ipalib_errors.NotFound as e:
+                msg = str(e)
+                if "%s not found" % _type in msg:
+                    return False
+                module.fail_json(msg="%s_show failed: %s" % (_type, msg))
+            return True
+
+        ipa_realm = module.ipa_get_realm()
+        _principal = []
+        for _princ in principal:
+            princ = _princ
+            realm = ipa_realm
+
+            # Get principal and realm from _princ if there is a realm
+            if '@' in _princ:
+                princ, realm = _princ.rsplit('@', 1)
+
+            # Lowercase principal
+            princ = princ.lower()
+
+            # Normalize principal
+            try:
+                nprinc = _normalize_principal_name(princ, realm)
+            except ipalib_errors.ValidationError as err:
+                module.fail_json(msg="%s: %s" % (_princ, str(err)))
+            princ = unicode(nprinc)
+
+            # Check that host principal exists
+            if princ.startswith("host/"):
+                if module.ipa_check_version("<", "4.9.0"):
+                    module.fail_json(
+                        msg="The use of host principals is not supported "
+                        "by your IPA version")
+
+                # Get host FQDN (no leading 'host/' and no trailing realm)
+                # (There is no removeprefix and removesuffix in Python2)
+                _host = princ[5:]
+                if _host.endswith("@%s" % realm):
+                    _host = _host[:-len(realm) - 1]
+
+                # Seach for host
+                if not _check_exists(module, "host", _host):
+                    module.fail_json(msg="Host '%s' does not exist" % _host)
+
+            # Check the service principal exists
+            else:
+                if not _check_exists(module, "service", princ):
+                    module.fail_json(msg="Service %s does not exist" % princ)
+
+            _principal.append(princ)
+
+        return _principal
+
     def exit_raw_json(module, **kwargs):
         """
         Print the raw parameters in JSON format, without masking.