Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sources/saml: Basic support for EncryptedAssertion element. #10099

Conversation

nicolas-semaphor
Copy link
Contributor

@nicolas-semaphor nicolas-semaphor commented Jun 13, 2024

Details

This PR aims to be an initial step in expanding Authentik's SAML2 Federation capabilities to adhere more fully to the entire SAML2 specification. It does so by providing the option to use an existing signing key-pair for requesting the Assertion element to be encrypted by the SAML IdP. I've attempted to make it such that existing SAML federations should not be disturbed by this change. Currently, the only way to enable that the Assertion should be encrypted, is by updating the "request_encrypted_assertions" model field to be True, instead of the default "False" value. I'm not sure how to update Authentik's UI but I'm hoping this is a small fix.

closes #7999

If the field is set to "True" for a SAMLSource, the metadata will be updated with an encryption key descriptor, as well as a new attribute in the SPSSO descriptor (WantAssertionsEncrypted="True"). This will inform the IdP that assertion should be encrypted. When the SAMLResponse is recieved, the _decrypt_response() method should locate and decrypt the EncryptedData in the EncryptedAssertion element and finally replacing the EncryptedAssertion with the decrypted Assertion. In this way, once the flow proceeds the SAMLResponse will appear as if it was never encrypted to begin with.

We have tested this decryption flow against the The Danish Agency for Digitization's Municipal Access Management service, that requires the service provider to request encrypted assertions.


Checklist

  • Local tests pass (ak test authentik/sources/saml)
  • The code has been formatted (make lint-fix) (EXCEPTION: In the metadata processor, the signing key descriptor method does not use the Union type, even though make lint-fix wants it to. I've chosen to adopt the same return type for the encryption key descriptor.)

If applicable

  • The documentation has been updated
  • The documentation has been formatted (make website)

@nicolas-semaphor nicolas-semaphor requested a review from a team as a code owner June 13, 2024 11:40
Copy link

netlify bot commented Jun 13, 2024

Deploy Preview for authentik-storybook ready!

Name Link
🔨 Latest commit c438557
🔍 Latest deploy log https://app.netlify.com/sites/authentik-storybook/deploys/66b3b00299e082000719425c
😎 Deploy Preview https://deploy-preview-10099--authentik-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

netlify bot commented Jun 13, 2024

Deploy Preview for authentik-docs canceled.

Name Link
🔨 Latest commit c438557
🔍 Latest deploy log https://app.netlify.com/sites/authentik-docs/deploys/66b3b002ef5e0d00087715e0

@nicolas-semaphor
Copy link
Contributor Author

This PR attempts to solve: #9172

@janhalen
Copy link

@BeryJu: What can we do to help this PR? Its currently a blocker in our project, not being able to communicate with The Danish Agency for Digitization's Municipal Access Management service.

@BeryJu
Copy link
Member

BeryJu commented Jun 25, 2024

Sorry I'm currently travelling and haven't been able to go through a bunch of PRs, I'll review this later this week/early next week when I get back home.

@janhalen
Copy link

Sorry I'm currently travelling and haven't been able to go through a bunch of PRs, I'll review this later this week/early next week when I get back home.

Thankyou for clearing this up! Safe travels!

Copy link

codecov bot commented Jul 3, 2024

Codecov Report

Attention: Patch coverage is 91.04478% with 6 lines in your changes missing coverage. Please review.

Project coverage is 92.62%. Comparing base (134caa9) to head (c438557).

Files Patch % Lines
authentik/sources/saml/processors/response.py 85.71% 3 Missing ⚠️
authentik/providers/saml/processors/assertion.py 60.00% 2 Missing ⚠️
authentik/sources/saml/processors/metadata.py 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10099      +/-   ##
==========================================
- Coverage   92.63%   92.62%   -0.01%     
==========================================
  Files         734      734              
  Lines       35980    36039      +59     
==========================================
+ Hits        33329    33381      +52     
- Misses       2651     2658       +7     
Flag Coverage Δ
e2e 49.41% <14.92%> (-0.09%) ⬇️
integration 25.18% <1.49%> (-0.04%) ⬇️
unit 90.17% <89.55%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@nicolas-semaphor
Copy link
Contributor Author

The lint check fails due to the Exception mentioned above.

EXCEPTION: In the metadata processor, the signing key descriptor method does not use the Union type, even though make lint-fix wants it to. I've chosen to adopt the same return type for the encryption key descriptor.

Maybe I can fix this by simply adding the # noqa comment to the line?
I'm unsure as to why the container build checks fail, seems I'm getting some 403 Status code on Post requests to ghcr.io/

Any thoughts?

@BeryJu BeryJu changed the title Sources/Saml: Basic support for EncryptedAssertion element. sources/saml: Basic support for EncryptedAssertion element. Jul 6, 2024
@BeryJu
Copy link
Member

BeryJu commented Jul 6, 2024

I can't push to the branch on the forked repo, but here's the diff for adding the option to the UI:

diff --git a/authentik/sources/saml/api/source.py b/authentik/sources/saml/api/source.py
index a3f0e9bd4..1af2a8795 100644
--- a/authentik/sources/saml/api/source.py
+++ b/authentik/sources/saml/api/source.py
@@ -32,6 +32,7 @@ class SAMLSourceSerializer(SourceSerializer):
             "digest_algorithm",
             "signature_algorithm",
             "temporary_user_delete_after",
+            "request_encrypted_assertions",
         ]
 
 
diff --git a/authentik/sources/saml/processors/metadata.py b/authentik/sources/saml/processors/metadata.py
index c12a55556..0820a3159 100644
--- a/authentik/sources/saml/processors/metadata.py
+++ b/authentik/sources/saml/processors/metadata.py
@@ -46,7 +46,7 @@ class MetadataProcessor:
             return key_descriptor
         return None
 
-    def get_encryption_key_descriptor(self) -> Optional[Element]: # noqa: UP007
+    def get_encryption_key_descriptor(self) -> Optional[Element]:  # noqa: UP007
         """Get Encryption KeyDescriptor, if encrypted assertion is requested"""
         if self.source.request_encrypted_assertions:
             key_descriptor = Element(f"{{{NS_SAML_METADATA}}}KeyDescriptor")
diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py
index 805f59ee4..5434a2c98 100644
--- a/authentik/sources/saml/processors/response.py
+++ b/authentik/sources/saml/processors/response.py
@@ -83,7 +83,6 @@ class ResponseProcessor:
 
     def _decrypt_response(self):
         """Decrypt SAMLResponse EncryptedAssertion Element"""
-
         manager = xmlsec.KeysManager()
         key = xmlsec.Key.from_memory(
             self._source.signing_kp.key_data,
@@ -93,9 +92,7 @@ class ResponseProcessor:
         manager.add_key(key)
         encryption_context = xmlsec.EncryptionContext(manager)
 
-        encrypted_assertion = self._root.find(
-            ".//{urn:oasis:names:tc:SAML:2.0:assertion}EncryptedAssertion"
-        )
+        encrypted_assertion = self._root.find(f".//{{{NS_SAML_ASSERTION}}}EncryptedAssertion")
         encrypted_data = xmlsec.tree.find_child(
             encrypted_assertion, "EncryptedData", xmlsec.constants.EncNs
         )
diff --git a/blueprints/schema.json b/blueprints/schema.json
index d32ba34e5..b321222ba 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -5106,6 +5106,11 @@
                     "minLength": 1,
                     "title": "Delete temporary users after",
                     "description": "Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually. (Format: hours=1;minutes=2;seconds=3)."
+                },
+                "request_encrypted_assertions": {
+                    "type": "boolean",
+                    "title": "Request encrypted assertions",
+                    "description": "When enabled, the SAML IdP will encrypt the assertion element using the public key of the SP signing keypair. The SAMLResponse will contain an EncryptedAssertion element, which will be decrypted by the private key of the service provider."
                 }
             },
             "required": []
diff --git a/schema.yml b/schema.yml
index 822318ad1..05fcb7384 100644
--- a/schema.yml
+++ b/schema.yml
@@ -43429,6 +43429,12 @@ components:
           description: 'Time offset when temporary users should be deleted. This only
             applies if your IDP uses the NameID Format ''transient'', and the user
             doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).'
+        request_encrypted_assertions:
+          type: boolean
+          description: When enabled, the SAML IdP will encrypt the assertion element
+            using the public key of the SP signing keypair. The SAMLResponse will
+            contain an EncryptedAssertion element, which will be decrypted by the
+            private key of the service provider.
     PatchedSCIMMappingRequest:
       type: object
       description: SCIMMapping Serializer
@@ -46114,6 +46120,12 @@ components:
           description: 'Time offset when temporary users should be deleted. This only
             applies if your IDP uses the NameID Format ''transient'', and the user
             doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).'
+        request_encrypted_assertions:
+          type: boolean
+          description: When enabled, the SAML IdP will encrypt the assertion element
+            using the public key of the SP signing keypair. The SAMLResponse will
+            contain an EncryptedAssertion element, which will be decrypted by the
+            private key of the service provider.
       required:
       - component
       - icon
@@ -46217,6 +46229,12 @@ components:
           description: 'Time offset when temporary users should be deleted. This only
             applies if your IDP uses the NameID Format ''transient'', and the user
             doesn''t log out manually. (Format: hours=1;minutes=2;seconds=3).'
+        request_encrypted_assertions:
+          type: boolean
+          description: When enabled, the SAML IdP will encrypt the assertion element
+            using the public key of the SP signing keypair. The SAMLResponse will
+            contain an EncryptedAssertion element, which will be decrypted by the
+            private key of the service provider.
       required:
       - name
       - pre_authentication_flow
diff --git a/web/src/admin/sources/saml/SAMLSourceForm.ts b/web/src/admin/sources/saml/SAMLSourceForm.ts
index c969411fb..945a74bfd 100644
--- a/web/src/admin/sources/saml/SAMLSourceForm.ts
+++ b/web/src/admin/sources/saml/SAMLSourceForm.ts
@@ -449,6 +449,25 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
                         >
                         </ak-radio>
                     </ak-form-element-horizontal>
+                    <ak-form-element-horizontal name="requestEncryptedAssertions">
+                        <label class="pf-c-switch">
+                            <input
+                                class="pf-c-switch__input"
+                                type="checkbox"
+                                ?checked=${first(this.instance?.requestEncryptedAssertions, true)}
+                            />
+                            <span class="pf-c-switch__toggle">
+                                <span class="pf-c-switch__toggle-icon">
+                                    <i class="fas fa-check" aria-hidden="true"></i>
+                                </span>
+                            </span>
+                            <span class="pf-c-switch__label"
+                                >${msg(
+                                    "Request Encrypted assertions from Identity provider.",
+                                )}</span
+                            >
+                        </label>
+                    </ak-form-element-horizontal>
                 </div>
             </ak-form-group>
             <ak-form-group>

One thing I would probably change about this is explicitly throw an error if request_encrypted_assertions is set but the assertion is not encrypted...but maybe that would warrant renaming request_encrypted_assertions to require_encrypted_assertions

Another thing, shouldn't the initial AuthnRequest request we build also be encrypted/encryptable? I assume that would be a future addition

@janhalen
Copy link

janhalen commented Jul 8, 2024

I can't push to the branch on the forked repo..

@BeryJu: You have been added as outside collaborator with write permissions to the fork...

@BeryJu BeryJu requested a review from a team as a code owner July 8, 2024 10:16
@nicolas-semaphor
Copy link
Contributor Author

Thank you very much for the UI additions - that will greatly simplify the process of setting up this type of SAML source.

One thing I would probably change about this is explicitly throw an error if request_encrypted_assertions is set but the assertion is not encrypted...but maybe that would warrant renaming request_encrypted_assertions to require_encrypted_assertions

Definitely a good addition. I'm fine with renaming the model field to reflect the requirement if need be.

Another thing, shouldn't the initial AuthnRequest request we build also be encrypted/encryptable? I assume that would be a future addition

In my understanding the initial AuthnRequest to the IdP is typically not encrypted, but I'll need to check if the IdP supports decryption AuthnRequests from service providers. The encryption layer is mainly to protect the sensitive information in the Assertion element.

(Please correct me if I'm wrong though!)

@BeryJu
Copy link
Member

BeryJu commented Jul 8, 2024

Thank you very much for the UI additions - that will greatly simplify the process of setting up this type of SAML source.

One thing I would probably change about this is explicitly throw an error if request_encrypted_assertions is set but the assertion is not encrypted...but maybe that would warrant renaming request_encrypted_assertions to require_encrypted_assertions

Definitely a good addition. I'm fine with renaming the model field to reflect the requirement if need be.

One thing that I noticed in the PR is that there's no explicit error handling, I'm assuming this would throw some xmlsec Error when the encryption is missing/invalid?

Another thing, shouldn't the initial AuthnRequest request we build also be encrypted/encryptable? I assume that would be a future addition

In my understanding the initial AuthnRequest to the IdP is typically not encrypted, but I'll need to check if the IdP supports decryption AuthnRequests from service providers. The encryption layer is mainly to protect the sensitive information in the Assertion element.

(Please correct me if I'm wrong though!)

That makes sense. One other thing I'd like to see for this is some added tests (we can also add those in this PR)

@nicolas-semaphor
Copy link
Contributor Author

nicolas-semaphor commented Jul 9, 2024

One thing that I noticed in the PR is that there's no explicit error handling, I'm assuming this would throw some xmlsec Error when the encryption is missing/invalid?

Something like this?

    def _decrypt_response(self):
        """Decrypt SAMLResponse EncryptedAssertion Element"""
        manager = xmlsec.KeysManager()
        key = xmlsec.Key.from_memory(
            self._source.signing_kp.key_data,
            xmlsec.constants.KeyDataFormatPem,
        )

        manager.add_key(key)
        encryption_context = xmlsec.EncryptionContext(manager)

        try:
            encrypted_assertion = self._root.find(f".//{{{NS_SAML_ASSERTION}}}EncryptedAssertion")
        except:
            raise MissingEncryptedAssertion()
        encrypted_data = xmlsec.tree.find_child(
            encrypted_assertion, "EncryptedData", xmlsec.constants.EncNs
        )
        try:
            decrypted_assertion = encryption_context.decrypt(encrypted_data)
        except:
            raise DecryptionError():

        index_of = self._root.index(encrypted_assertion)
        self._root.remove(encrypted_assertion)
        self._root.insert(
            index_of,
            decrypted_assertion,
        )
        LOGGER.debug("Successfully decrypted EncryptedAssertion element")

With the following added to source/saml/exceptions.py ...

class MissingEncryptedAssertion(SAMLException):
    """Exception raised when SAMLResponse does not contain EncryptedAssertion element"""

class DecryptionError(SAMLException):
    """Exception raised when decryption of SAMLResponse could not be performed"""

If this is acceptable I can commit&push, but please let me know if it's too simple.
Regards,
Nicolas

@BeryJu
Copy link
Member

BeryJu commented Jul 9, 2024

yeah pretty much what you said, only thing is we usually try to prevent bare excepts (i've pushed some stuff)

one other thing I'm noticing while testing this is that I think it might be a good option to add another certificate setting to the source to configure the signature and encryption certificate separately, as I'm testing against keycloak which by default generates two different certificates for each

@nicolas-semaphor
Copy link
Contributor Author

nicolas-semaphor commented Jul 10, 2024

yeah pretty much what you said, only thing is we usually try to prevent bare excepts (i've pushed some stuff)

one other thing I'm noticing while testing this is that I think it might be a good option to add another certificate setting to the source to configure the signature and encryption certificate separately, as I'm testing against keycloak which by default generates two different certificates for each

This would be the right way to do it, I only initially planned to re-use the signing keypair for simplicity, but It would be best to have the choice between:

  • No Encryption
  • Encryption using signing-keypair
  • Encryption using a second 'encryption-keypair'

EDIT: A simpler solution could maybe be just to replicate what is done for the signing certificate?
That is imply having a drop down menu, where no selection means no Encryption, and choosing a keypair will enable the decryption flow. Then the user would just be able to choose the signing keypair for simplicity, or generate a second keypair for encryption. Thoughts?

EDITEDIT: I realize this changes the implementation we've slowly been building now, but maybe it's better this way?

Kind Regards,
Nicolas

@BeryJu
Copy link
Member

BeryJu commented Jul 24, 2024

yeah pretty much what you said, only thing is we usually try to prevent bare excepts (i've pushed some stuff)
one other thing I'm noticing while testing this is that I think it might be a good option to add another certificate setting to the source to configure the signature and encryption certificate separately, as I'm testing against keycloak which by default generates two different certificates for each

This would be the right way to do it, I only initially planned to re-use the signing keypair for simplicity, but It would be best to have the choice between:

  • No Encryption
  • Encryption using signing-keypair
  • Encryption using a second 'encryption-keypair'

EDIT: A simpler solution could maybe be just to replicate what is done for the signing certificate? That is imply having a drop down menu, where no selection means no Encryption, and choosing a keypair will enable the decryption flow. Then the user would just be able to choose the signing keypair for simplicity, or generate a second keypair for encryption. Thoughts?

EDITEDIT: I realize this changes the implementation we've slowly been building now, but maybe it's better this way?

Kind Regards, Nicolas

Sorry didnt see your edits, yeah I think what you suggested in the first Edit makes most sense

@nicolas-semaphor
Copy link
Contributor Author

yeah pretty much what you said, only thing is we usually try to prevent bare excepts (i've pushed some stuff)
one other thing I'm noticing while testing this is that I think it might be a good option to add another certificate setting to the source to configure the signature and encryption certificate separately, as I'm testing against keycloak which by default generates two different certificates for each

This would be the right way to do it, I only initially planned to re-use the signing keypair for simplicity, but It would be best to have the choice between:

  • No Encryption
  • Encryption using signing-keypair
  • Encryption using a second 'encryption-keypair'

EDIT: A simpler solution could maybe be just to replicate what is done for the signing certificate? That is imply having a drop down menu, where no selection means no Encryption, and choosing a keypair will enable the decryption flow. Then the user would just be able to choose the signing keypair for simplicity, or generate a second keypair for encryption. Thoughts?
EDITEDIT: I realize this changes the implementation we've slowly been building now, but maybe it's better this way?
Kind Regards, Nicolas

Sorry didnt see your edits, yeah I think what you suggested in the first Edit makes most sense

Roger that, will see if I can't figure out how to replicate it.

@nicolas-semaphor
Copy link
Contributor Author

Something like this @BeryJu ?

@BeryJu BeryJu self-assigned this Aug 1, 2024
@BeryJu BeryJu force-pushed the 9172-backend-support-for-encrypted-assertions branch from 94c86ea to 6915421 Compare August 7, 2024 12:57
@BeryJu BeryJu requested a review from a team as a code owner August 7, 2024 13:03
nicolas-semaphor and others added 15 commits August 7, 2024 19:33
Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
…e it's not in the schema

Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
Signed-off-by: Jens Langhammer <[email protected]>
@BeryJu BeryJu force-pushed the 9172-backend-support-for-encrypted-assertions branch from 7fd8c3d to c438557 Compare August 7, 2024 17:33
@BeryJu BeryJu merged commit 19c3f7d into goauthentik:main Aug 7, 2024
65 of 66 checks passed
kensternberg-authentik added a commit that referenced this pull request Aug 7, 2024
* main:
  web/admin: refactor property mappings forms (#10810)
  web: bump API Client version (#10811)
  sources/saml: Basic support for EncryptedAssertion element. (#10099)
  web: bump API Client version (#10809)
  sources: add property mappings for all oauth and saml sources (#8771)
  web: bump API Client version (#10808)
  stages/authenticator: add created, last_updated and last_used metadata (#10636)
  providers/proxy: avoid erroring on logout with session_id is None (#9119)
  core: bump google-api-python-client from 2.139.0 to 2.140.0 (#10802)
  core: bump pyyaml from 6.0.1 to 6.0.2 (#10803)
  core: bump django from 5.0.7 to 5.0.8 (#10804)
  core: bump goauthentik.io/api/v3 from 3.2024063.1 to 3.2024063.2 (#10805)
  web: bump @sentry/browser from 8.23.0 to 8.24.0 in /web in the sentry group across 1 directory (#10806)
  web: bump the wdio group across 2 directories with 2 updates (#10807)
kensternberg-authentik added a commit that referenced this pull request Aug 8, 2024
* main: (25 commits)
  website/docs: add link from Install docs to Enterprise docs (#10827)
  website/docs: new upgrade page (#10742)
  stages/authenticator: actually update last_used (#10813)
  sources/ldap: Add enabled filter for ldap_password_validate signal (#10823)
  web: bump API Client version (#10821)
  sources/plex: add property mappings (#10772)
  core: bump goauthentik.io/api/v3 from 3.2024063.2 to 3.2024063.5 (#10817)
  web: bump the wdio group across 2 directories with 4 updates (#10818)
  web: bump chromedriver from 127.0.1 to 127.0.2 in /tests/wdio (#10819)
  web: update to ESLint 9 (#10812)
  website/docs: add source property mappings, rework provider property mappings (#10652)
  web/admin: refactor property mappings forms (#10810)
  web: bump API Client version (#10811)
  sources/saml: Basic support for EncryptedAssertion element. (#10099)
  web: bump API Client version (#10809)
  sources: add property mappings for all oauth and saml sources (#8771)
  web: bump API Client version (#10808)
  stages/authenticator: add created, last_updated and last_used metadata (#10636)
  providers/proxy: avoid erroring on logout with session_id is None (#9119)
  core: bump google-api-python-client from 2.139.0 to 2.140.0 (#10802)
  ...
@janhalen janhalen deleted the 9172-backend-support-for-encrypted-assertions branch September 13, 2024 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Signed and Encrypted SAML request?
3 participants