diff --git a/README.md b/README.md index 4ff2affd0f..a2f00e1bd1 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ addon | version | maintainers | summary [auth_oauth_multi_token](auth_oauth_multi_token/) | 18.0.2.0.0 | | Allow multiple connection with the same OAuth account [auth_oidc](auth_oidc/) | 18.0.1.0.0 | sbidoul | Allow users to login through OpenID Connect Provider [auth_oidc_environment](auth_oidc_environment/) | 18.0.1.0.0 | | This module allows to use server env for OIDC configuration -[auth_saml](auth_saml/) | 18.0.1.0.3 | vincent-hatakeyama | SAML2 Authentication +[auth_saml](auth_saml/) | 18.0.1.1.0 | vincent-hatakeyama | SAML2 Authentication [auth_session_timeout](auth_session_timeout/) | 18.0.1.0.0 | | This module disable all inactive sessions since a given delay [auth_signup_verify_email](auth_signup_verify_email/) | 18.0.1.0.0 | | Force uninvited users to use a good email for signup [auth_user_case_insensitive](auth_user_case_insensitive/) | 18.0.1.0.0 | | Makes the user login field case insensitive diff --git a/auth_saml/README.rst b/auth_saml/README.rst index fa34def2bb..b5da3b32ca 100644 --- a/auth_saml/README.rst +++ b/auth_saml/README.rst @@ -11,7 +11,7 @@ SAML2 Authentication !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:6c12eb5e5c1c80ee3a898dfd7985b6f518a5e0dbad26db680d94dd9759a57699 + !! source digest: sha256:3e5b3bb4044a255d619b8bda0861d58722133ab48366ffd2c2f88c8fbdcc0a5c !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -86,6 +86,10 @@ query parameter ``disable_autoredirect``, as in also displayed if there is an error with SAML login, in order to display any error message. +If you are using Office365 as identity provider, set up the federation +metadata document rather than the document itself. This will allow the +module to refresh the document when needed. + Usage ===== diff --git a/auth_saml/__manifest__.py b/auth_saml/__manifest__.py index 86c79c7f9b..a31681fc5c 100644 --- a/auth_saml/__manifest__.py +++ b/auth_saml/__manifest__.py @@ -4,7 +4,7 @@ { "name": "SAML2 Authentication", - "version": "18.0.1.0.3", + "version": "18.0.1.1.0", "category": "Tools", "author": "XCG Consulting, Odoo Community Association (OCA)", "maintainers": ["vincent-hatakeyama"], diff --git a/auth_saml/i18n/auth_saml.pot b/auth_saml/i18n/auth_saml.pot index f97886f39a..5db7654952 100644 --- a/auth_saml/i18n/auth_saml.pot +++ b/auth_saml/i18n/auth_saml.pot @@ -192,6 +192,11 @@ msgstr "" msgid "Identity Provider Metadata" msgstr "" +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__idp_metadata_url +msgid "Identity Provider Metadata URL" +msgstr "" + #. module: auth_saml #: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form msgid "Identity Provider Settings" @@ -344,6 +349,11 @@ msgstr "" msgid "Providers" msgstr "" +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Refresh" +msgstr "" + #. module: auth_saml #: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form #: model_terms:ir.ui.view,arch_db:auth_saml.view_users_form @@ -427,6 +437,14 @@ msgstr "" msgid "Signature Algorithm" msgstr "" +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__idp_metadata_url +msgid "" +"Some SAML providers, notably Office365 can have a metadata document which " +"changes over time, and they provide a URL to the document instead. When this" +" field is set, the metadata can be fetched from the provided URL." +msgstr "" + #. module: auth_saml #: model:ir.model,name:auth_saml.model_ir_config_parameter msgid "System Parameter" @@ -445,6 +463,14 @@ msgstr "" msgid "The current SAML token in use" msgstr "" +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"The metadata may be automatically redownloaded from that URL. If that's okay" +" for you and you provider gives you a URL, use this field " +"preferably. Otherwise use the next one" +msgstr "" + #. module: auth_saml #. odoo-python #: code:addons/auth_saml/models/res_users.py:0 diff --git a/auth_saml/i18n/es.po b/auth_saml/i18n/es.po index 6dc88be8d5..2f7e3ef71b 100644 --- a/auth_saml/i18n/es.po +++ b/auth_saml/i18n/es.po @@ -24,7 +24,6 @@ msgstr "- o -" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Access Denied" msgstr "Acceso Denegado" @@ -214,6 +213,11 @@ msgstr "Atributo de la respuesta del PDI" msgid "Identity Provider Metadata" msgstr "Metadatos del proveedor de identidad" +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__idp_metadata_url +msgid "Identity Provider Metadata URL" +msgstr "" + #. module: auth_saml #: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form msgid "Identity Provider Settings" @@ -316,7 +320,6 @@ msgstr "URL de metadatos" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Missing parameters" msgstr "Parámetros que faltan" @@ -377,6 +380,11 @@ msgstr "Nombre de Proveedor" msgid "Providers" msgstr "Proveedores" +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Refresh" +msgstr "" + #. module: auth_saml #: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form #: model_terms:ir.ui.view,arch_db:auth_saml.view_users_form @@ -452,7 +460,6 @@ msgstr "Firmar metadatos" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Sign up is not allowed on this database." msgstr "No está permitido registrarse en esta base de datos." @@ -461,6 +468,14 @@ msgstr "No está permitido registrarse en esta base de datos." msgid "Signature Algorithm" msgstr "Algoritmo de firma" +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__idp_metadata_url +msgid "" +"Some SAML providers, notably Office365 can have a metadata document which " +"changes over time, and they provide a URL to the document instead. When this " +"field is set, the metadata can be fetched from the provided URL." +msgstr "" + #. module: auth_saml #: model:ir.model,name:auth_saml.model_ir_config_parameter msgid "System Parameter" @@ -483,10 +498,17 @@ msgstr "" msgid "The current SAML token in use" msgstr "El código SAML actual en uso" +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"The metadata may be automatically redownloaded from that URL. If that's okay " +"for you and you provider gives you a URL, use this field " +"preferably. Otherwise use the next one" +msgstr "" + #. module: auth_saml #. odoo-python #: code:addons/auth_saml/models/res_users.py:0 -#, python-format msgid "" "This database disallows users to have both passwords and SAML IDs. Error for " "logins %s" @@ -497,7 +519,6 @@ msgstr "" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Unknown provider" msgstr "Proveedor desconocido" @@ -549,7 +570,6 @@ msgstr "Si la solicitud debe firmarse o no" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "You do not have access to this database. Please contact support." msgstr "" "No tiene acceso a esta base de datos. Póngase en contacto con el servicio de " diff --git a/auth_saml/i18n/fr.po b/auth_saml/i18n/fr.po index 7a5f2d6e35..734184f5e9 100644 --- a/auth_saml/i18n/fr.po +++ b/auth_saml/i18n/fr.po @@ -25,7 +25,6 @@ msgstr "- ou -" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Access Denied" msgstr "Accès refusé" @@ -218,6 +217,11 @@ msgstr "Attribut de réponse du FI" msgid "Identity Provider Metadata" msgstr "Métadonnées du fournisseur d’identité" +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__idp_metadata_url +msgid "Identity Provider Metadata URL" +msgstr "" + #. module: auth_saml #: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form msgid "Identity Provider Settings" @@ -323,7 +327,6 @@ msgstr "Adresse universelle des métadonnées" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Missing parameters" msgstr "Paramètres manquants" @@ -385,6 +388,11 @@ msgstr "Nom du fournisseur" msgid "Providers" msgstr "Fournisseurs" +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Refresh" +msgstr "" + #. module: auth_saml #: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form #: model_terms:ir.ui.view,arch_db:auth_saml.view_users_form @@ -460,7 +468,6 @@ msgstr "Signer les métadonnées" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Sign up is not allowed on this database." msgstr "L’inscription n’est pas autorisée sur cette base de donnée." @@ -469,6 +476,14 @@ msgstr "L’inscription n’est pas autorisée sur cette base de donnée." msgid "Signature Algorithm" msgstr "Algorithme de signature" +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__idp_metadata_url +msgid "" +"Some SAML providers, notably Office365 can have a metadata document which " +"changes over time, and they provide a URL to the document instead. When this " +"field is set, the metadata can be fetched from the provided URL." +msgstr "" + #. module: auth_saml #: model:ir.model,name:auth_saml.model_ir_config_parameter msgid "System Parameter" @@ -491,10 +506,17 @@ msgstr "" msgid "The current SAML token in use" msgstr "Le jeton SAML en cours d’utilisation" +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"The metadata may be automatically redownloaded from that URL. If that's okay " +"for you and you provider gives you a URL, use this field " +"preferably. Otherwise use the next one" +msgstr "" + #. module: auth_saml #. odoo-python #: code:addons/auth_saml/models/res_users.py:0 -#, python-format msgid "" "This database disallows users to have both passwords and SAML IDs. Error for " "logins %s" @@ -505,7 +527,6 @@ msgstr "" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Unknown provider" msgstr "Fournisseur inconnu" @@ -557,7 +578,6 @@ msgstr "Signature ou non des requêtes" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "You do not have access to this database. Please contact support." msgstr "" "Vous n’avez pas accès à cette base de donnée. Veuillez contacter votre " diff --git a/auth_saml/i18n/it.po b/auth_saml/i18n/it.po index 565cab3e35..34f751ef82 100644 --- a/auth_saml/i18n/it.po +++ b/auth_saml/i18n/it.po @@ -24,7 +24,6 @@ msgstr "- o -" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Access Denied" msgstr "Accesso negato" @@ -211,6 +210,11 @@ msgstr "Attributo risposta IDP" msgid "Identity Provider Metadata" msgstr "Metadati provider identità" +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__idp_metadata_url +msgid "Identity Provider Metadata URL" +msgstr "" + #. module: auth_saml #: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form msgid "Identity Provider Settings" @@ -312,7 +316,6 @@ msgstr "URL metadati" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Missing parameters" msgstr "Parametri mancanti" @@ -374,6 +377,11 @@ msgstr "Nome provider" msgid "Providers" msgstr "Provider" +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Refresh" +msgstr "" + #. module: auth_saml #: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form #: model_terms:ir.ui.view,arch_db:auth_saml.view_users_form @@ -449,7 +457,6 @@ msgstr "Frma metadati" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Sign up is not allowed on this database." msgstr "Non è consentito iscriversi a questo database." @@ -458,6 +465,14 @@ msgstr "Non è consentito iscriversi a questo database." msgid "Signature Algorithm" msgstr "Algoritmo firma" +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__idp_metadata_url +msgid "" +"Some SAML providers, notably Office365 can have a metadata document which " +"changes over time, and they provide a URL to the document instead. When this " +"field is set, the metadata can be fetched from the provided URL." +msgstr "" + #. module: auth_saml #: model:ir.model,name:auth_saml.model_ir_config_parameter msgid "System Parameter" @@ -480,10 +495,17 @@ msgstr "" msgid "The current SAML token in use" msgstr "Token SAML attualmente in uso" +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"The metadata may be automatically redownloaded from that URL. If that's okay " +"for you and you provider gives you a URL, use this field " +"preferably. Otherwise use the next one" +msgstr "" + #. module: auth_saml #. odoo-python #: code:addons/auth_saml/models/res_users.py:0 -#, python-format msgid "" "This database disallows users to have both passwords and SAML IDs. Error for " "logins %s" @@ -494,7 +516,6 @@ msgstr "" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "Unknown provider" msgstr "Provider sconosciuto" @@ -546,7 +567,6 @@ msgstr "Se la richiesta deve essere firmata o meno" #. module: auth_saml #. odoo-python #: code:addons/auth_saml/controllers/main.py:0 -#, python-format msgid "You do not have access to this database. Please contact support." msgstr "Non si ha accesso a questo database. Contattare il supporto." diff --git a/auth_saml/models/auth_saml_provider.py b/auth_saml/models/auth_saml_provider.py index 9b8c2d7294..77262cf1d1 100644 --- a/auth_saml/models/auth_saml_provider.py +++ b/auth_saml/models/auth_saml_provider.py @@ -7,15 +7,19 @@ import logging import os import tempfile -import urllib.parse +import urllib + +import requests # dependency name is pysaml2 # pylint: disable=W7936 import saml2 import saml2.xmldsig as ds from saml2.client import Saml2Client from saml2.config import Config as Saml2Config +from saml2.sigver import SignatureError from odoo import api, fields, models +from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -42,6 +46,14 @@ class AuthSamlProvider(models.Model): ), required=True, ) + idp_metadata_url = fields.Char( + string="Identity Provider Metadata URL", + help="Some SAML providers, notably Office365 can have a metadata " + "document which changes over time, and they provide a URL to the " + "document instead. When this field is set, the metadata can be " + "fetched from the provided URL.", + ) + sp_baseurl = fields.Text( string="Override Base URL", help="""Base URL sent to Odoo with this, rather than automatically @@ -232,10 +244,19 @@ def _get_config_for_provider(self, base_url: str = None) -> Saml2Config: "cert_file": self._get_cert_key_path("sp_pem_public"), "key_file": self._get_cert_key_path("sp_pem_private"), } - sp_config = Saml2Config() - sp_config.load(settings) - sp_config.allow_unknown_attributes = True - return sp_config + try: + sp_config = Saml2Config() + sp_config.load(settings) + sp_config.allow_unknown_attributes = True + return sp_config + except saml2.SAMLError: + if self.env.context.get("saml2_retry_after_refresh_metadata", False): + raise + # Retry after refresh metadata + self.action_refresh_metadata_from_url() + return self.with_context( + saml2_retry_after_refresh_metatata=1 + )._get_config_for_provider(base_url) def _get_client_for_provider(self, base_url: str = None) -> Saml2Client: sp_config = self._get_config_for_provider(base_url) @@ -280,13 +301,26 @@ def _get_auth_request(self, extra_state=None, url_root=None): def _validate_auth_response(self, token: str, base_url: str = None): """return the validation data corresponding to the access token""" self.ensure_one() - - client = self._get_client_for_provider(base_url) - response = client.parse_authn_request_response( - token, - saml2.entity.BINDING_HTTP_POST, - self._get_outstanding_requests_dict(), - ) + try: + client = self._get_client_for_provider(base_url) + response = client.parse_authn_request_response( + token, + saml2.entity.BINDING_HTTP_POST, + self._get_outstanding_requests_dict(), + ) + except SignatureError: + # we have a metadata url: try to refresh the metadata document + if self.idp_metadata_url: + self.action_refresh_metadata_from_url() + # retry: if it fails again, we let the exception flow + client = self._get_client_for_provider(base_url) + response = client.parse_authn_request_response( + token, + saml2.entity.BINDING_HTTP_POST, + self._get_outstanding_requests_dict(), + ) + else: + raise matching_value = None if self.matching_attribute == "subject.nameId": @@ -370,3 +404,43 @@ def _hook_validate_auth_response(self, response, matching_value): vals[attribute.field_name] = attribute_value return {"mapped_attrs": vals} + + def action_refresh_metadata_from_url(self): + providers = self.search( + [("idp_metadata_url", "ilike", "http%"), ("id", "in", self.ids)] + ) + if not providers: + return False + + providers_to_update = {} + for provider in providers: + document = requests.get(provider.idp_metadata_url, timeout=5) + if document.status_code != 200: + raise UserError( + "Unable to download the metadata for " + f"{provider.name}: {document.reason}" + ) + if document.text != provider.idp_metadata: + providers_to_update[provider.id] = document.text + + if not providers_to_update: + return False + + # lock the records we might update, so that multiple simultaneous login + # attempts will not cause concurrent updates + provider_ids = tuple(providers_to_update.keys()) + self.env.cr.execute( + "SELECT id FROM auth_saml_provider WHERE id in %s FOR UPDATE", + (provider_ids,), + ) + updated = False + for provider in providers: + if provider.id in providers_to_update: + provider.idp_metadata = providers_to_update[provider.id] + _logger.info( + "Updated metadata for provider %s", + provider.name, + ) + updated = True + + return updated diff --git a/auth_saml/readme/CONFIGURE.md b/auth_saml/readme/CONFIGURE.md index 68072d142c..6d69ef00e9 100644 --- a/auth_saml/readme/CONFIGURE.md +++ b/auth_saml/readme/CONFIGURE.md @@ -18,3 +18,8 @@ query parameter `disable_autoredirect`, as in `https://example.com/web/login?disable_autoredirect=` The login is also displayed if there is an error with SAML login, in order to display any error message. + +If you are using Office365 as identity provider, set up the federation metadata document +rather than the document itself. This will allow the module to refresh the document when +needed. + diff --git a/auth_saml/tests/data/cert_idp_expired.pem b/auth_saml/tests/data/cert_idp_expired.pem new file mode 100644 index 0000000000..d2c320a4b9 --- /dev/null +++ b/auth_saml/tests/data/cert_idp_expired.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID7TCCAtWgAwIBAgIUDBX/LJ1BPZOhb2vrDnwIasyEi+AwDQYJKoZIhvcNAQEL +BQAwgYUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMQ4wDAYDVQQH +DAVQYXJpczEMMAoGA1UECgwDT0NBMQwwCgYDVQQLDANPQ0ExFDASBgNVBAMMC2V4 +YW1wbGUuY29tMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMB4XDTIz +MDEwMTExMDAyN1oXDTIzMDEzMTExMDAyN1owgYUxCzAJBgNVBAYTAkFVMRMwEQYD +VQQIDApTb21lLVN0YXRlMQ4wDAYDVQQHDAVQYXJpczEMMAoGA1UECgwDT0NBMQww +CgYDVQQLDANPQ0ExFDASBgNVBAMMC2V4YW1wbGUuY29tMR8wHQYJKoZIhvcNAQkB +FhB0ZXN0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvgeLRr1Q9aS/t8ZuC7/pZRHTB6sqamVwXyR7zh0v51yH7xBy9xs4zJWKneRn +OJw46IogYhY+dyNWElbY+Ckcc6z1eJONiHNtOKAy07VtfhisGviRv1kLE56SHGgW +fIXrOuFqj6F1yTfKyLtq2RZBzmbMTNG7z89rO2hqdTWqhyof9OGWtecrM7Ei9PnL +tqULhQyh6n47KnIXfBMLIeQG7d/WyGU5CnO7yhHkS/51E9gI6g5G0VoueBVFErCl +rjo0clMJrFVpanOG2USGgLfPkomSIv9ZL4SreFN27sbhTbkVWxbk7AOCFCQcaBIv +RThpRrA9YRv2dB/X4yIi7UrrPwIDAQABo1MwUTAdBgNVHQ4EFgQU4WFoM/SL6qvT +jV4YUwH3rggBqyIwHwYDVR0jBBgwFoAU4WFoM/SL6qvTjV4YUwH3rggBqyIwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAYG+CTENdcmiKmyYg/n6H +59RRgaLHSszjsKYdM5Uy/PmwfvPTRuxP38UhF0gWJTsLxWRjI0ejzdy5vzvxpoYl +3pEyYxPzl+jfGA6AUSOfzTT/ksi7SZWak3cqJDFCdnfaCSjYyi+nKz5y6Bm1/fMy +/3zJX92ELIWc8cTUItUIWjlITmxgLhIGr1wYaCinxkaEopGqkX85RYFaWKyYa5ok +8MnoYbPrh/i9EekHUMBMPKWN3tWMMEROTtX9hmxSSTtgdQCahBaOCCU+m8PSNKEc +UA8nSStaolv8t6aOyEb/Kzs7WSbd7v1ovZsy2FYmIRn0eHz8fpMAw2qk7mol6iao +GQ== +-----END CERTIFICATE----- diff --git a/auth_saml/tests/data/key_idp_expired.pem b/auth_saml/tests/data/key_idp_expired.pem new file mode 100644 index 0000000000..496f309a12 --- /dev/null +++ b/auth_saml/tests/data/key_idp_expired.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+B4tGvVD1pL+3 +xm4Lv+llEdMHqypqZXBfJHvOHS/nXIfvEHL3GzjMlYqd5Gc4nDjoiiBiFj53I1YS +Vtj4KRxzrPV4k42Ic204oDLTtW1+GKwa+JG/WQsTnpIcaBZ8hes64WqPoXXJN8rI +u2rZFkHOZsxM0bvPz2s7aGp1NaqHKh/04Za15yszsSL0+cu2pQuFDKHqfjsqchd8 +Ewsh5Abt39bIZTkKc7vKEeRL/nUT2AjqDkbRWi54FUUSsKWuOjRyUwmsVWlqc4bZ +RIaAt8+SiZIi/1kvhKt4U3buxuFNuRVbFuTsA4IUJBxoEi9FOGlGsD1hG/Z0H9fj +IiLtSus/AgMBAAECggEBAKuXUFJeHL7TNzMRAMmnT28uOypPiwtr8Z5X6Vtiy6DU +0wIyDj3H3PAPkI2mcvaRSmngYAFyKJGX3N7OgTkElmZ1pWptgn3WDKf3MC4vQ2F7 +kd0A20q3cuMSaskvzC5BFvmiFoD/wMYjlP7RDVhdWqqv9IbhVAAAQcnxLUANZ6CH +/xrieGuYavs62pSu5fnke7zRozdD1Mb7/oolAnycaLuoi1eZBh8wW8EJyFSxcZ5A +pYF5kNqbwAdOZ22Tygxwu7lnh8PUOKxf9pTmO6uUYAJcn/Z3ZHtnBYsjU/LkfNPV +hYLu1bKftm6UEZYwCXE3/ygop1q648NvCvtJB+Gbj9ECgYEA8nB+hS+7MLgi/dv8 +FCMJ9HBN76/nlwjOCTZIyIhCs5Jc6zJQGiDNLUFM/1mpBKUWWAss3g0dmJq32ish +apsCUxabzWuKi44fDMEterJrGDWquyJK+jNPqfqOORLdMf0edNfZbjUxev7D52Ak +4Ej3Ggy/fENd8QWLK6PZHV5X1MUCgYEAyKiWlawh7l8eBrba8UFQ4n1HiK/2uEud +yQOLceSRmW/xC6ZCiR0ILinrtZWRxqQg+ZSS24hjnHhcdnRw8TRXx22TkTwGfAXW +wKesPrtGJrn0ADuZwPkGewyeHPsisXNSiuGLPcLiOCoNNYgbIWJ2RknM1Xw+2p8C +qYU8Si6l6DMCgYEA20v4ld7sExCszjZ72XcsXQhs5v+Vm9/iByEsSwA+XZJqLHFx +VYEQNvxXeq8OnN37zR4msqDogY6J+XWEH5shSiksO28ofj3LRk1DJzZWeyqoSeem +LJXXXKkAlw3COaJ9NzG8Qt0o6dmjORqVoK8/nTekyfFh+0+JaKsoDFG3XwUCgYBN +tq2Ljj0d+wzAAPXO1kMjVO3tjGj7e53CinLpS2LwkCBFKMFAJVRTvLyjeSgaTNrQ +jrBKAgrCQQNehT5wzJrqjA/JAfxo8EH6H3ZgXVuQCBjuNicYS9ossfhStRj8rPNd +AnlRFDdVFUREZVBMn7u7AT4puJMHTOpVCVsOR/7NbQKBgApyR1WfsxZYi8vzVosQ +jnMIW18lnZN3s6auyEvmpVowx0U0yd9QU3HHX1j8Kfq7D9uERCwBtIfn9fzZrZnu +Xgbi9LMUT1z1jFXxJNgzaNmm0JHW9cD24BWNeQ60uxaRiGGmCyfmgqrGOXSn2R8w +KoWEnnunZ9nehcD9dkWcH5zG +-----END PRIVATE KEY----- diff --git a/auth_saml/tests/fake_idp.py b/auth_saml/tests/fake_idp.py index 6e15e00a49..ceac2fa761 100644 --- a/auth_saml/tests/fake_idp.py +++ b/auth_saml/tests/fake_idp.py @@ -116,8 +116,9 @@ def set_identity(self, identity): class FakeIDP(Server): - def __init__(self, metadatas=None): - settings = CONFIG + def __init__(self, metadatas=None, settings=None): + if settings is None: + settings = CONFIG if metadatas: settings.update({"metadata": {"inline": metadatas}}) @@ -172,3 +173,13 @@ def authn_request_endpoint(self, req, binding, relay_state): ) return DummyResponse(**_dict) + + +class UnsignedFakeIDP(FakeIDP): + def create_authn_response( + self, + *args, + **kwargs, + ): + kwargs["sign_assertion"] = False + return super().create_authn_response(*args, **kwargs) diff --git a/auth_saml/tests/test_pysaml.py b/auth_saml/tests/test_pysaml.py index 54e9a3fbb4..ddc6412f36 100644 --- a/auth_saml/tests/test_pysaml.py +++ b/auth_saml/tests/test_pysaml.py @@ -2,15 +2,21 @@ import base64 import html import os +import os.path as osp import urllib +from copy import deepcopy from unittest.mock import patch +import responses +from saml2.sigver import SignatureError + from odoo.exceptions import AccessDenied, UserError, ValidationError from odoo.tests import HttpCase, tagged +from odoo.tools import mute_logger from odoo.addons.auth_saml.controllers.main import fragment_to_query_string -from .fake_idp import DummyResponse, FakeIDP +from .fake_idp import CONFIG, DummyResponse, FakeIDP, UnsignedFakeIDP @tagged("saml", "post_install", "-at_install") @@ -708,3 +714,125 @@ def test_menu_redirect(self): response.url, self.base_url() + "/odoo#menu_id=12", ) + + @responses.activate + def test_download_metadata(self): + expected_metadata = self.idp.get_metadata() + responses.add( + responses.GET, + "http://localhost:8000/metadata", + status=200, + content_type="text/xml", + body=expected_metadata, + ) + self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata" + self.saml_provider.idp_metadata = "" + self.saml_provider.action_refresh_metadata_from_url() + self.assertEqual(self.saml_provider.idp_metadata, expected_metadata) + + @responses.activate + def test_download_metadata_no_provider(self): + self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata" + self.saml_provider.idp_metadata = "" + self.saml_provider.active = False + self.saml_provider.action_refresh_metadata_from_url() + self.assertFalse(self.saml_provider.idp_metadata) + + @responses.activate + def test_download_metadata_error(self): + responses.add( + responses.GET, + "http://localhost:8000/metadata", + status=500, + content_type="text/xml", + ) + self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata" + self.saml_provider.idp_metadata = "" + with self.assertRaises(UserError): + self.saml_provider.action_refresh_metadata_from_url() + self.assertFalse(self.saml_provider.idp_metadata) + + @responses.activate + def test_download_metadata_no_update(self): + expected_metadata = self.idp.get_metadata() + responses.add( + responses.GET, + "http://localhost:8000/metadata", + status=200, + content_type="text/xml", + body=expected_metadata, + ) + self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata" + self.saml_provider.idp_metadata = expected_metadata + self.saml_provider.action_refresh_metadata_from_url() + self.assertEqual(self.saml_provider.idp_metadata, expected_metadata) + + @responses.activate + def test_login_with_saml_metadata_empty(self): + self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata" + self.saml_provider.idp_metadata = "" + expected_metadata = self.idp.get_metadata() + responses.add( + responses.GET, + "http://localhost:8000/metadata", + status=200, + content_type="text/xml", + body=expected_metadata, + ) + self.test_login_with_saml() + self.assertEqual(self.saml_provider.idp_metadata, expected_metadata) + + @responses.activate + def test_login_with_saml_metadata_key_changed(self): + settings = deepcopy(CONFIG) + settings["key_file"] = osp.join( + osp.dirname(__file__), "data", "key_idp_expired.pem" + ) + settings["cert"] = osp.join( + osp.dirname(__file__), "data", "key_idp_expired.pem" + ) + expired_idp = FakeIDP(settings=settings) + self.saml_provider.idp_metadata = expired_idp.get_metadata() + self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata" + up_to_date_metadata = self.idp.get_metadata() + self.assertNotEqual(self.saml_provider.idp_metadata, up_to_date_metadata) + responses.add( + responses.GET, + "http://localhost:8000/metadata", + status=200, + content_type="text/xml", + body=up_to_date_metadata, + ) + self.test_login_with_saml() + + @responses.activate + def test_login_with_saml_unsigned_response(self): + self.add_provider_to_user() + self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata" + unsigned_idp = UnsignedFakeIDP([self.saml_provider._metadata_string()]) + redirect_url = self.saml_provider._get_auth_request() + self.assertIn("http://localhost:8000/sso/redirect?SAMLRequest=", redirect_url) + + response = unsigned_idp.fake_login(redirect_url) + self.assertEqual(200, response.status_code) + unpacked_response = response._unpack() + + responses.add( + responses.GET, + "http://localhost:8000/metadata", + status=200, + content_type="text/xml", + body=self.saml_provider.idp_metadata, + ) + with ( + self.assertRaises(SignatureError), + mute_logger("saml2.entity"), + mute_logger("saml2.client_base"), + ): + (database, login, token) = ( + self.env["res.users"] + .sudo() + .auth_saml( + self.saml_provider.id, unpacked_response.get("SAMLResponse"), None + ) + ) diff --git a/auth_saml/views/auth_saml.xml b/auth_saml/views/auth_saml.xml index e9ed50207a..0fab93dadf 100644 --- a/auth_saml/views/auth_saml.xml +++ b/auth_saml/views/auth_saml.xml @@ -76,6 +76,21 @@ + +