Skip to content

Commit 17c0ce8

Browse files
Merge pull request #172 from skoranda/common_domain_cookie
Add capability for common domain cookie Based on spec: https://www.oasis-open.org/committees/download.php/56782/sstc-saml-profiles-errata-2.0-wd-07.pdf
2 parents efec162 + 569a842 commit 17c0ce8

File tree

2 files changed

+77
-2
lines changed

2 files changed

+77
-2
lines changed

example/plugins/frontends/saml2_frontend.yaml.example

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,14 @@ config:
5252
"https://accounts.google.com": LoA1
5353

5454
endpoints:
55-
single_sign_on_service: {'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST': sso/post,
56-
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect': sso/redirect}
55+
single_sign_on_service:
56+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST': sso/post
57+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect': sso/redirect
58+
59+
# If configured and not false or empty the common domain cookie _saml_idp will be set
60+
# with or have appended the IdP used for authentication. The default is not to set the
61+
# cookie. If the value is a dictionary with key 'domain' then the domain for the cookie
62+
# will be set to the value for the 'domain' key. If no 'domain' is set then the domain
63+
# from the BASE defined for the proxy will be used.
64+
common_domain_cookie:
65+
domain: .example.com

src/satosa/frontends/saml2.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
import json
77
import logging
88
from base64 import urlsafe_b64decode
9+
from base64 import urlsafe_b64encode
10+
from urllib.parse import quote
11+
from urllib.parse import unquote
912
from urllib.parse import urlparse
13+
from http.cookies import SimpleCookie
1014

1115
from saml2 import SAMLError, xmldsig
1216
from saml2.config import IdPConfig
@@ -359,6 +363,12 @@ def _handle_authn_response(self, context, internal_response, idp):
359363
http_args = idp.apply_binding(
360364
resp_args["binding"], str(resp), resp_args["destination"],
361365
request_state["relay_state"], response=True)
366+
367+
# Set the common domain cookie _saml_idp if so configured.
368+
if 'common_domain_cookie' in self.config:
369+
if self.config['common_domain_cookie']:
370+
self._set_common_domain_cookie(internal_response, http_args, context)
371+
362372
del context.state[self.name]
363373
return make_saml_response(resp_args["binding"], http_args)
364374

@@ -427,6 +437,62 @@ def _register_endpoints(self, providers):
427437

428438
return url_map
429439

440+
def _set_common_domain_cookie(self, internal_response, http_args, context):
441+
"""
442+
"""
443+
# Find any existing common domain cookie and deconsruct it to
444+
# obtain the list of IdPs.
445+
cookie = SimpleCookie(context.cookie)
446+
if '_saml_idp' in cookie:
447+
common_domain_cookie = cookie['_saml_idp']
448+
satosa_logging(logger, logging.DEBUG, "Found existing common domain cookie {}".format(common_domain_cookie), context.state)
449+
space_separated_b64_idp_string = unquote(common_domain_cookie.value)
450+
b64_idp_list = space_separated_b64_idp_string.split()
451+
idp_list = [urlsafe_b64decode(b64_idp).decode('utf-8') for b64_idp in b64_idp_list]
452+
else:
453+
satosa_logging(logger, logging.DEBUG, "No existing common domain cookie found", context.state)
454+
idp_list = []
455+
456+
satosa_logging(logger, logging.DEBUG, "Common domain cookie list of IdPs is {}".format(idp_list), context.state)
457+
458+
# Identity the current IdP just used for authentication in this flow.
459+
this_flow_idp = internal_response.to_dict()['auth_info']['issuer']
460+
461+
# Remove all occurrences of the current IdP from the list of IdPs.
462+
idp_list = [idp for idp in idp_list if idp != this_flow_idp]
463+
464+
# Append the current IdP.
465+
idp_list.append(this_flow_idp)
466+
satosa_logging(logger, logging.DEBUG, "Added IdP {} to common domain cookie list of IdPs".format(this_flow_idp), context.state)
467+
satosa_logging(logger, logging.DEBUG, "Common domain cookie list of IdPs is now {}".format(idp_list), context.state)
468+
469+
# Construct the cookie.
470+
b64_idp_list = [urlsafe_b64encode(idp.encode()).decode("utf-8") for idp in idp_list]
471+
space_separated_b64_idp_string = " ".join(b64_idp_list)
472+
url_encoded_space_separated_b64_idp_string = quote(space_separated_b64_idp_string)
473+
474+
cookie = SimpleCookie()
475+
cookie['_saml_idp'] = url_encoded_space_separated_b64_idp_string
476+
cookie['_saml_idp']['path'] = '/'
477+
478+
# Use the domain from configuration if present else use the domain
479+
# from the base URL for the front end.
480+
domain = urlparse(self.base_url).netloc
481+
if isinstance(self.config['common_domain_cookie'], dict):
482+
if 'domain' in self.config['common_domain_cookie']:
483+
domain = self.config['common_domain_cookie']['domain']
484+
485+
# Ensure that the domain begins with a '.'
486+
if domain[0] != '.':
487+
domain = '.' + domain
488+
489+
cookie['_saml_idp']['domain'] = domain
490+
cookie['_saml_idp']['secure'] = True
491+
492+
# Set the cookie.
493+
satosa_logging(logger, logging.DEBUG, "Setting common domain cookie with {}".format(cookie.output()), context.state)
494+
http_args['headers'].append(tuple(cookie.output().split(": ", 1)))
495+
430496
def _build_idp_config_endpoints(self, config, providers):
431497
"""
432498
Builds the final frontend module config

0 commit comments

Comments
 (0)