Skip to content

Commit

Permalink
[IMP] auth_saml: download the provider metadata
Browse files Browse the repository at this point in the history
On Office365, what you get when configuring an application for SAML
authentication is the URL of the federation metadata document. This URL
is stable, but the content of the document is not. I suspect some of the
encryption keys can be updated / renewed over time. The result is that
the configured provider in Odoo suddenly stops working, because the
messages sent by the Office365 provider can no longer be validated by
Odoo (because the federation document is out of date). Downloading the
new version and updating the auth.saml.provider record fixes the issue.

This PR adds a new field to store the URL of the metadata document. When
this field is set on a provider, you get a button next to it in the form
view to download the document from the URL. The button will not update
the document if it has not changed.

Additionally, when a SignatureError happens, we check if downloading the
document again fixes the issue.
  • Loading branch information
gurneyalex committed Jan 9, 2024
1 parent bf2852e commit 0bed723
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 8 deletions.
2 changes: 1 addition & 1 deletion auth_oidc/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Authentication OpenID Connect
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:71510d7bf0aa7f001922c23a7610ad556deef38538d265989fb70ddc010547d6
!! source digest: sha256:c0b511a2aa2ce3715f6c903369015852d573c98a028d2d9919c6b789578e2d21
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
Expand Down
2 changes: 1 addition & 1 deletion auth_oidc/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ <h1 class="title">Authentication OpenID Connect</h1>
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:71510d7bf0aa7f001922c23a7610ad556deef38538d265989fb70ddc010547d6
!! source digest: sha256:c0b511a2aa2ce3715f6c903369015852d573c98a028d2d9919c6b789578e2d21
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-auth/tree/15.0/auth_oidc"><img alt="OCA/server-auth" src="https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-auth-15-0/server-auth-15-0-auth_oidc"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-auth&amp;target_branch=15.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows users to login through an OpenID Connect provider using the
Expand Down
6 changes: 5 additions & 1 deletion auth_saml/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SAML2 Authentication
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:749f38cb523fb18981d9e229196105be0adc619147af5f55e3c887b25ca86dc0
!! source digest: sha256:56a6042e204ca8c553db8eb36de4b1ad7ae8e1e9d5abe598a8398f5e17da7e7f
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
Expand Down Expand Up @@ -81,6 +81,10 @@ by using the 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.

Usage
=====

Expand Down
60 changes: 55 additions & 5 deletions auth_saml/models/auth_saml_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
import tempfile
import urllib.parse

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__)

Expand All @@ -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
Expand Down Expand Up @@ -282,11 +294,24 @@ def _validate_auth_response(self, token: str, base_url: str = None):
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:
response = client.parse_authn_request_response(
token,
saml2.entity.BINDING_HTTP_POST,
self._get_outstanding_requests_dict(),
)
except SignatureError:

Check warning on line 303 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L303

Added line #L303 was not covered by tests
# we have a metadata url: try to refresh the metadata document
if self.idp_metadata_url:
self.action_refresh_metadata_from_url()

Check warning on line 306 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L306

Added line #L306 was not covered by tests
# retry: if it fails again, we let the exception flow
response = client.parse_authn_request_response(

Check warning on line 308 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L308

Added line #L308 was not covered by tests
token,
saml2.entity.BINDING_HTTP_POST,
self._get_outstanding_requests_dict(),
)
else:
raise

Check warning on line 314 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L314

Added line #L314 was not covered by tests
matching_value = None

if self.matching_attribute == "subject.nameId":
Expand Down Expand Up @@ -370,3 +395,28 @@ 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(

Check warning on line 400 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L400

Added line #L400 was not covered by tests
[("idp_metadata_url", "ilike", "http%"), ("id", "in", self.ids)]
)
if not providers:
return False

Check warning on line 404 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L404

Added line #L404 was not covered by tests
# lock the records we might update, so that multiple simultaneous login
# attempts will not cause concurrent updates
self.env.cr.execute(

Check warning on line 407 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L407

Added line #L407 was not covered by tests
"SELECT id FROM auth_saml_provider WHERE id in %s FOR UPDATE",
(tuple(providers.ids),),
)
updated = False

Check warning on line 411 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L411

Added line #L411 was not covered by tests
for provider in providers:
document = requests.get(provider.idp_metadata_url)

Check warning on line 413 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L413

Added line #L413 was not covered by tests
if document.status_code != 200:
raise UserError(

Check warning on line 415 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L415

Added line #L415 was not covered by tests
f"Unable to download the metadata for {provider.name}: {document.reason}"
)
if document.text != provider.idp_metadata:
provider.idp_metadata = document.text
_logger.info("Updated provider metadata for %s", provider.name)
updated = True
return updated

Check warning on line 422 in auth_saml/models/auth_saml_provider.py

View check run for this annotation

Codecov / codecov/patch

auth_saml/models/auth_saml_provider.py#L419-L422

Added lines #L419 - L422 were not covered by tests
4 changes: 4 additions & 0 deletions auth_saml/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ with the highest priority. It is still possible to access the login without redi
by using the 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.
15 changes: 15 additions & 0 deletions auth_saml/views/auth_saml.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@
</div>
<group name="idp_settings">
<group string="Identity Provider Settings">
<span>
<label for="idp_metadata_url" />
<div>
<field name="idp_metadata_url" />
<p
class="help small"
>If you provider gives you a URL, use this field preferably</p>
</div>
<button
type="object"
string="Refresh"
name="action_refresh_metadata_from_url"
attrs="{'invisible': [('idp_metadata_url', '=', False)]}"
/>
</span>
<label for="idp_metadata" />
<div>
<field
Expand Down

0 comments on commit 0bed723

Please sign in to comment.