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 | | 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 | | SAML2 Authentication
+[auth_saml](auth_saml/) | 18.0.1.1.0 | | 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 @@
+
+
+
+
+
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