9
9
import tempfile
10
10
import urllib .parse
11
11
12
+ import requests
13
+
12
14
# dependency name is pysaml2 # pylint: disable=W7936
13
15
import saml2
14
16
import saml2 .xmldsig as ds
15
17
from saml2 .client import Saml2Client
16
18
from saml2 .config import Config as Saml2Config
19
+ from saml2 .sigver import SignatureError
17
20
18
21
from odoo import api , fields , models
22
+ from odoo .exceptions import UserError
19
23
20
24
_logger = logging .getLogger (__name__ )
21
25
@@ -42,6 +46,14 @@ class AuthSamlProvider(models.Model):
42
46
),
43
47
required = True ,
44
48
)
49
+ idp_metadata_url = fields .Char (
50
+ string = "Identity Provider Metadata URL" ,
51
+ help = "Some SAML providers, notably Office365 can have a metadata "
52
+ "document which changes over time, and they provide a URL to the "
53
+ "document instead. When this field is set, the metadata can be "
54
+ "fetched from the provided URL." ,
55
+ )
56
+
45
57
sp_baseurl = fields .Text (
46
58
string = "Override Base URL" ,
47
59
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:
232
244
"cert_file" : self ._get_cert_key_path ("sp_pem_public" ),
233
245
"key_file" : self ._get_cert_key_path ("sp_pem_private" ),
234
246
}
235
- sp_config = Saml2Config ()
236
- sp_config .load (settings )
237
- sp_config .allow_unknown_attributes = True
238
- return sp_config
247
+ try :
248
+ sp_config = Saml2Config ()
249
+ sp_config .load (settings )
250
+ sp_config .allow_unknown_attributes = True
251
+ return sp_config
252
+ except saml2 .SAMLError :
253
+ if self .env .context .get ("saml2_retry_after_refresh_metadata" , False ):
254
+ raise
255
+ # Retry after refresh metadata
256
+ self .action_refresh_metadata_from_url ()
257
+ return self .with_context (
258
+ saml2_retry_after_refresh_metatata = 1
259
+ )._get_config_for_provider (base_url )
239
260
240
261
def _get_client_for_provider (self , base_url : str = None ) -> Saml2Client :
241
262
sp_config = self ._get_config_for_provider (base_url )
@@ -280,13 +301,26 @@ def _get_auth_request(self, extra_state=None, url_root=None):
280
301
def _validate_auth_response (self , token : str , base_url : str = None ):
281
302
"""return the validation data corresponding to the access token"""
282
303
self .ensure_one ()
283
-
284
- client = self ._get_client_for_provider (base_url )
285
- response = client .parse_authn_request_response (
286
- token ,
287
- saml2 .entity .BINDING_HTTP_POST ,
288
- self ._get_outstanding_requests_dict (),
289
- )
304
+ try :
305
+ client = self ._get_client_for_provider (base_url )
306
+ response = client .parse_authn_request_response (
307
+ token ,
308
+ saml2 .entity .BINDING_HTTP_POST ,
309
+ self ._get_outstanding_requests_dict (),
310
+ )
311
+ except SignatureError :
312
+ # we have a metadata url: try to refresh the metadata document
313
+ if self .idp_metadata_url :
314
+ self .action_refresh_metadata_from_url ()
315
+ # retry: if it fails again, we let the exception flow
316
+ client = self ._get_client_for_provider (base_url )
317
+ response = client .parse_authn_request_response (
318
+ token ,
319
+ saml2 .entity .BINDING_HTTP_POST ,
320
+ self ._get_outstanding_requests_dict (),
321
+ )
322
+ else :
323
+ raise
290
324
matching_value = None
291
325
292
326
if self .matching_attribute == "subject.nameId" :
@@ -370,3 +404,38 @@ def _hook_validate_auth_response(self, response, matching_value):
370
404
vals [attribute .field_name ] = attribute_value
371
405
372
406
return {"mapped_attrs" : vals }
407
+
408
+ def action_refresh_metadata_from_url (self ):
409
+ providers = self .search (
410
+ [("idp_metadata_url" , "ilike" , "http%" ), ("id" , "in" , self .ids )]
411
+ )
412
+ if not providers :
413
+ return False
414
+
415
+ providers_to_update = {}
416
+ for provider in providers :
417
+ document = requests .get (provider .idp_metadata_url , timeout = 5 )
418
+ if document .status_code != 200 :
419
+ raise UserError (
420
+ f"Unable to download the metadata for { provider .name } : { document .reason } "
421
+ )
422
+ if document .text != provider .idp_metadata :
423
+ providers_to_update [provider .id ] = document .text
424
+
425
+ # lock the records we might update, so that multiple simultaneous login
426
+ # attempts will not cause concurrent updates
427
+ self .env .cr .execute (
428
+ "SELECT id FROM auth_saml_provider WHERE id in %s FOR UPDATE" ,
429
+ (tuple (providers_to_update .keys ()),),
430
+ )
431
+ updated = False
432
+ for provider in providers :
433
+ if provider .id in providers_to_update :
434
+ provider .idp_metadata = providers_to_update [provider .id ]
435
+ _logger .info (
436
+ "Updated metadata for provider %s from %s" ,
437
+ provider .name ,
438
+ )
439
+ updated = True
440
+
441
+ return updated
0 commit comments