Skip to content

Commit 71e114d

Browse files
committed
Redo the configurable memoization capability
This commit introduces four optional configuration parameters that can be used to modify the default SATOSA behaviour: - CONTEXT_STATE_DELETE - memorize_disco_idp - use_memorized_disco_idp_when_force_authn - mirror_saml_force_authn By default, SATOSA deletes the context state when it receives an authentication response from an identity provider. The first configuration option, CONTEXT_STATE_DELETE, allows us disable this behaviour and thus keep state across different authentication flows, while the user uses the same browser. The second configuration option, memorize_disco_idp, controls whether SATOSA will remember and reuse the IdP that is selected from a discovery service. If ForceAuthn is set in the authentication request, then the user will be redirected to the discovery service (if it is configured) and ForceAuthn will be set in the authentication request towards the selected IdP. These two options together allow us to modify the current behaviour so that within a given session, a user will select only once the identity provider and then SATOSA will store this information in the state cookie. When the cookie expires, the user will be redirected again to the discovery service. The third configuration option, use_memorized_disco_idp_when_force_authn, controls whether SATOSA will skip the discovery service, even when memorize_disco_idp is set, for the current session there is an entity id of an IdP stored in the cookie state, and ForceAuthn is requested. SPs that need to force a new IdP selection (e.g. for account linking purposes) should set this option to False, in order to be able to use ForceAuthn to redirect the user to the discovery service. The fourth configuration option, mirror_saml_force_authn, adds configuration option to mirror ForceAuthn. By default, when the SATOSA SAML frontend receives a SAML authentication request with ForceAuthn set to `True`, this information is not mirrored in the SAML authentication request that is generated by the SATOSA SAML backend towards the upstream identity provider. If the configuration parameter `mirror_saml_force_authn` is set to `True`, then the default behaviour changes and the SATOSA SAML backend will set ForceAuthn to true when it proxies a SAML authentication request with ForceAuthn set to `True`. The default values of these configuration options are tuned so that the default behaviour of SATOSA is not changed. Signed-off-by: Ivan Kanakarakis <[email protected]>
1 parent 77f32ee commit 71e114d

File tree

9 files changed

+163
-204
lines changed

9 files changed

+163
-204
lines changed

doc/README.md

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ in the [example directory](../example).
3737
| -------------- | --------- | ------------- | ----------- |
3838
| `BASE` | string | `https://proxy.example.com` | base url of the proxy |
3939
| `COOKIE_STATE_NAME` | string | `satosa_state` | name of cooke SATOSA uses for preserving state between requests |
40-
| `CONTEXT_STATE_DELETE` | bool | `True` | controls whether SATOSA will delete the state after receiving the authentication response from the upstream IdP|
40+
| `CONTEXT_STATE_DELETE` | bool | `True` | controls whether SATOSA will delete the state cookie after receiving the authentication response from the upstream IdP|
4141
| `STATE_ENCRYPTION_KEY` | string | `52fddd3528a44157` | key used for encrypting the state cookie, will be overriden by the environment variable `SATOSA_STATE_ENCRYPTION_KEY` if it is set |
4242
| `INTERNAL_ATTRIBUTES` | string | `example/internal_attributes.yaml` | path to attribute mapping
4343
| `CUSTOM_PLUGIN_MODULE_PATHS` | string[] | `[example/plugins/backends, example/plugins/frontends]` | list of directory paths containing any front-/backend plugin modules |
@@ -233,7 +233,7 @@ in its UI. The following flow diagram shows the communcation:
233233
`SP -> optional discovery service -> selected proxy SAML entity -> target IdP`
234234

235235
3. The **SAMLVirtualCoFrontend** module enables multiple IdP frontends, each with its own distinct
236-
entityID and SSO endpoints, and each representing a distinct collaborative organization or CO.
236+
entityID and SSO endpoints, and each representing a distinct collaborative organization or CO.
237237
An example configuration can be found [here](../example/plugins/frontends/saml2_virtualcofrontend.yaml.example).
238238

239239
The following flow diagram shows the communication:
@@ -323,43 +323,57 @@ config:
323323
disco_srv: http://disco.example.com
324324
```
325325

326-
##### Remember the IdP provided in the discovery service
326+
##### Mirror the SAML ForceAuthn option
327+
328+
By default when the SAML frontend receives a SAML authentication request
329+
with `ForceAuthn` set to `True`, this information is not mirrored in the SAML
330+
authentication request that is generated by the SAML backend towards the
331+
upstream identity provider. If the configuration option
332+
`mirror_saml_force_authn` is set to `True`, then the default behaviour changes
333+
and the SAML backend will set `ForceAuthn` to true when it proxies a SAML
334+
authentication request with `ForceAuthn` set to `True`.
327335

328-
The `remember_selected_idp_from_disco` parameter controls whether the user will have to always select a
329-
target provider when a discovery service is configured. If the parameter is set to `True` and ForceAutn is not set,
330-
SATOSA will remember and reuse the selected target provider for the duration that context.state is valid.
331336
The default behaviour is `False`.
332337

333338
```yaml
334339
config:
335-
sp_config: [...]
336-
remember_selected_idp_from_disco: True
340+
mirror_saml_force_authn: True
341+
[...]
337342
```
338343

339-
##### Use the configured discovery service if ForceAuthn is set to true
344+
##### Memorize the IdP selected through the discovery service
345+
346+
In the classic flow, the user is asked to select their home organization to
347+
authenticate to. The `memorize_disco_idp` configuration option controls whether
348+
the user will have to always select a target provider when a discovery service
349+
is configured. If the parameter is set to `True` (and `ForceAuthn` is not set),
350+
the proxy will remember and reuse the selected target provider for the duration
351+
that the state cookie is valid. If `ForceAuthn` is set, then the
352+
`use_memorized_disco_idp_when_force_authn` configuration option can overide
353+
this property and still reuse the selected target provider.
340354

341-
The `use_disco_when_forceauthn` parameter controls whether the user will be redirected to the configured
342-
discovery service when the SP sends a SAML authentication request with `ForceAuthn` set to `True`. The
343-
default behaviour is `False`.
355+
The default behaviour is `False`.
344356

345357
```yaml
346358
config:
347-
sp_config: [...]
348-
use_disco_when_forceauthn: True
359+
memorize_disco_idp: True
360+
[...]
349361
```
350362

351-
##### Mirror the SAML ForceAuthn option
363+
##### Use the configured discovery service if ForceAuthn is set to true
352364

353-
By default when the SATOSA SAML frontend receives a SAML authentication request with ForceAuthn set to `True`,
354-
this information is not mirrored in the SAML authentication request that is generated by the SATOSA SAML backend
355-
towards the upstream identity provider. If the configuration parameter `mirror_saml_forceauthn` is set to `True`,
356-
then the default behaviour changes and the SATOSA SAML backend will set ForceAuthn to true when it proxies a SAML
357-
authentication request with ForceAuthn set to `True`. The default behaviour is `False`.
365+
The `use_memorized_disco_idp_when_force_authn` configuration option controls
366+
whether the user will skip the configured discovery service when the SP sends a
367+
SAML authentication request with `ForceAuthn` set to `True` but the proxy has
368+
memorized the user's previous selection.
369+
370+
The default behaviour is `False`.
358371

359372
```yaml
360373
config:
361-
sp_config: [...]
362-
mirror_saml_forceauthn: True
374+
memorize_disco_idp: True
375+
use_memorized_disco_idp_when_force_authn: True
376+
[...]
363377
```
364378

365379
### <a name="openid_plugin" style="color:#000000">OpenID Connect plugins</a>

example/plugins/backends/saml2_backend.yaml.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ module: satosa.backends.saml2.SAMLBackend
22
name: Saml2
33
config:
44
idp_blacklist_file: /path/to/blacklist.json
5+
6+
mirror_saml_force_authn: no
7+
memorize_disco_idp: no
8+
use_memorized_disco_idp_when_force_authn: no
9+
510
sp_config:
6-
remember_selected_idp_from_disco: False
711
key_file: backend.key
812
cert_file: backend.crt
913
organization: {display_name: Example Identities, name: Example Identities Org., url: 'http://www.example.com'}

example/proxy_conf.yaml.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
BASE: https://example.com
33
INTERNAL_ATTRIBUTES: "internal_attributes.yaml"
44
COOKIE_STATE_NAME: "SATOSA_STATE"
5-
CONTEXT_STATE_DELETE: False
5+
CONTEXT_STATE_DELETE: yes
66
STATE_ENCRYPTION_KEY: "asdASD123"
77
CUSTOM_PLUGIN_MODULE_PATHS:
88
- "plugins/backends"

src/satosa/backends/saml2.py

Lines changed: 65 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ class SAMLBackend(BackendModule, SAMLBaseModule):
4343
KEY_SAML_DISCOVERY_SERVICE_URL = 'saml_discovery_service_url'
4444
KEY_SAML_DISCOVERY_SERVICE_POLICY = 'saml_discovery_service_policy'
4545
KEY_SP_CONFIG = 'sp_config'
46-
KEY_SELECTED_IDP_FROM_DISCO = 'selected_idp_from_disco'
47-
KEY_REMEMBER_SELECTED_IDP_FROM_DISCO = 'remember_selected_idp_from_disco'
48-
KEY_USE_DISCO_WHEN_FORCEAUTHN = 'use_disco_when_forceauthn'
49-
KEY_MIRROR_SAML_FORCEAUTHN = 'mirror_saml_forceauthn'
46+
KEY_MIRROR_SAML_FORCE_AUTHN = 'mirror_saml_force_authn'
47+
KEY_MEMORIZE_DISCO_IDP = 'memorize_disco_idp'
48+
KEY_USE_MEMORIZED_DISCO_IDP_WHEN_FORCE_AUTHN = 'use_memorized_disco_idp_when_force_authn'
49+
5050
VALUE_ACR_COMPARISON_DEFAULT = 'exact'
5151

5252
def __init__(self, outgoing, internal_attributes, config, base_url, name):
@@ -68,15 +68,12 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name):
6868
super().__init__(outgoing, internal_attributes, base_url, name)
6969
self.config = self.init_config(config)
7070

71-
sp_config = SPConfig().load(copy.deepcopy(config[self.KEY_SP_CONFIG]), False)
71+
sp_config = SPConfig().load(copy.deepcopy(
72+
config[SAMLBackend.KEY_SP_CONFIG]), False
73+
)
7274
self.sp = Base(sp_config)
7375

74-
# If use_disco_when_forceauthn is not set in the SP config,
75-
# then False is the default behaviour
76-
if self.KEY_USE_DISCO_WHEN_FORCEAUTHN not in self.config['sp_config']:
77-
self.config['sp_config'][self.KEY_USE_DISCO_WHEN_FORCEAUTHN] = False
78-
79-
self.discosrv = config.get(self.KEY_DISCO_SRV)
76+
self.discosrv = config.get(SAMLBackend.KEY_DISCO_SRV)
8077
self.encryption_keys = []
8178
self.outstanding_queries = {}
8279
self.idp_blacklist_file = config.get('idp_blacklist_file', None)
@@ -103,46 +100,51 @@ def get_idp_entity_id(self, context):
103100
:return: the entity_id of the idp or None
104101
"""
105102

106-
# XXX TODO should not look into sp_config
107-
# XXX TODO make sense of the config options
108-
109-
# if there is only one IdP in the metadata, bypass the discovery service
110103
idps = self.sp.metadata.identity_providers()
111-
if len(idps) == 1 and "mdq" not in self.config["sp_config"]["metadata"]:
104+
only_one_idp_in_metadata = (
105+
len(idps) == 1 and "mdq" not in self.config["sp_config"]["metadata"]
106+
)
107+
108+
target_entity_id = context.get_decoration(Context.KEY_TARGET_ENTITYID)
109+
110+
force_authn = context.get_decoration(Context.KEY_FORCE_AUTHN)
111+
memorized_disco_idp = (
112+
self.config.get(SAMLBackend.KEY_MEMORIZE_DISCO_IDP)
113+
and context.state.get(Context.KEY_MEMORIZED_DISCO_IDP)
114+
)
115+
use_memorized_disco_idp_when_force_authn = self.config.get(
116+
SAMLBackend.KEY_USE_MEMORIZED_DISCO_IDP_WHEN_FORCE_AUTHN
117+
)
118+
use_memorized_disco_idp = memorized_disco_idp and (
119+
not force_authn or use_memorized_disco_idp_when_force_authn
120+
)
121+
122+
if only_one_idp_in_metadata:
112123
entity_id = idps[0]
113-
# if the user has selected an IdP and it is available in the context.state,
114-
# then set entity_id to that unless ForceAuthn is set to true and
115-
# use_disco_when_forcauthn is false
116-
elif (
117-
self.config["sp_config"].get(self.KEY_REMEMBER_SELECTED_IDP_FROM_DISCO)
118-
and self.KEY_SELECTED_IDP_FROM_DISCO in context.state
119-
120-
and not context.get_decoration(Context.KEY_FORCE_AUTHN)
121-
):
122-
satosa_logging(
123-
logger, logging.INFO,
124-
"Bypassing discovery service. Using IdP %s" %
125-
context.state[self.KEY_SELECTED_IDP_FROM_DISCO],
126-
context.state,
127-
)
128-
entity_id = context.state[self.KEY_SELECTED_IDP_FROM_DISCO]
129-
elif (
130-
self.config["sp_config"].get(self.KEY_REMEMBER_SELECTED_IDP_FROM_DISCO)
131-
and self.KEY_SELECTED_IDP_FROM_DISCO in context.state
132-
133-
and context.get_decoration(Context.KEY_FORCE_AUTHN)
134-
and not self.config["sp_config"][self.KEY_USE_DISCO_WHEN_FORCEAUTHN]
135-
):
136-
satosa_logging(
137-
logger, logging.INFO,
138-
"Bypassing discovery service. Using IdP %s" %
139-
context.state[self.KEY_SELECTED_IDP_FROM_DISCO],
140-
context.state,
141-
)
142-
entity_id = context.state[self.KEY_SELECTED_IDP_FROM_DISCO]
124+
elif use_memorized_disco_idp:
125+
entity_id = memorized_disco_idp
126+
elif target_entity_id:
127+
entity_id = target_entity_id
143128
else:
144-
entity_id = context.get_decoration(Context.KEY_TARGET_ENTITYID)
145-
129+
entity_id = None
130+
131+
satosa_logging(
132+
logger, logging.INFO,
133+
{
134+
"message": "Selected IdP entity ID",
135+
"idps": idps,
136+
"only_one_idp_in_metadata": only_one_idp_in_metadata,
137+
"force_authn": force_authn,
138+
"memorized_disco_idp": memorized_disco_idp,
139+
"use_memorized_disco_idp_when_force_authn": (
140+
use_memorized_disco_idp_when_force_authn
141+
),
142+
"use_memorized_disco_idp": use_memorized_disco_idp,
143+
"target_entity_id": target_entity_id,
144+
"entity_id": entity_id,
145+
},
146+
context.state,
147+
)
146148
return entity_id
147149

148150
def start_auth(self, context, internal_req):
@@ -181,9 +183,12 @@ def disco_query(self, context):
181183
return_url = endpoints["discovery_response"][0][0]
182184

183185
disco_url = (
184-
context.get_decoration(self.KEY_SAML_DISCOVERY_SERVICE_URL) or self.discosrv
186+
context.get_decoration(SAMLBackend.KEY_SAML_DISCOVERY_SERVICE_URL)
187+
or self.discosrv
188+
)
189+
disco_policy = context.get_decoration(
190+
SAMLBackend.KEY_SAML_DISCOVERY_SERVICE_POLICY
185191
)
186-
disco_policy = context.get_decoration(self.KEY_SAML_DISCOVERY_SERVICE_POLICY)
187192

188193
args = {"return": return_url}
189194
if disco_policy:
@@ -214,17 +219,6 @@ def construct_requested_authn_context(self, entity_id):
214219

215220
return authn_context
216221

217-
def mirror_saml_forceauthn(self, context, kwargs):
218-
if (self.KEY_MIRROR_SAML_FORCEAUTHN in self.config['sp_config']
219-
and self.config['sp_config'][self.KEY_MIRROR_SAML_FORCEAUTHN]):
220-
# If ForceAuthn is found in the state cookie, use that
221-
if (Context.KEY_FORCE_AUTHN in context.state
222-
and context.state[Context.KEY_FORCE_AUTHN] == 'true'):
223-
kwargs['force_authn'] = context.state[Context.KEY_FORCE_AUTHN]
224-
elif context.get_decoration(Context.KEY_FORCE_AUTHN) == 'true':
225-
kwargs['force_authn'] = context.get_decoration(Context.KEY_FORCE_AUTHN)
226-
return kwargs
227-
228222
def authn_request(self, context, entity_id):
229223
"""
230224
Do an authorization request on idp with given entity id.
@@ -251,9 +245,12 @@ def authn_request(self, context, entity_id):
251245
kwargs = {}
252246
authn_context = self.construct_requested_authn_context(entity_id)
253247
if authn_context:
254-
kwargs['requested_authn_context'] = authn_context
255-
256-
kwargs = self.mirror_saml_forceauthn(context, kwargs)
248+
kwargs["requested_authn_context"] = authn_context
249+
if self.config.get(SAMLBackend.KEY_MIRROR_SAML_FORCE_AUTHN):
250+
kwargs["force_authn"] = (
251+
context.state.get(Context.KEY_FORCE_AUTHN)
252+
or context.get_decoration(Context.KEY_FORCE_AUTHN)
253+
)
257254

258255
try:
259256
binding, destination = self.sp.pick_binding(
@@ -321,9 +318,8 @@ def authn_response(self, context, binding):
321318

322319
context.decorate(Context.KEY_BACKEND_METADATA_STORE, self.sp.metadata)
323320

324-
del context.state[self.name]
325-
# we should not remember ForceAuthn any longer
326-
context.state[Context.KEY_FORCE_AUTHN] = None
321+
context.state.pop(self.name, None)
322+
context.state.pop(Context.KEY_FORCE_AUTHN, None)
327323
return self.auth_callback_func(context, self._translate_response(authn_response, context.state))
328324

329325
def disco_response(self, context):
@@ -345,9 +341,8 @@ def disco_response(self, context):
345341
satosa_logging(logger, logging.DEBUG, "No IDP chosen for state", state, exc_info=True)
346342
raise SATOSAAuthenticationError(state, "No IDP chosen") from err
347343

348-
if (self.KEY_REMEMBER_SELECTED_IDP_FROM_DISCO in self.config['sp_config']
349-
and self.config['sp_config'][self.KEY_REMEMBER_SELECTED_IDP_FROM_DISCO]):
350-
context.state[self.KEY_SELECTED_IDP_FROM_DISCO] = entity_id
344+
if self.config.get(SAMLBackend.KEY_MEMORIZE_DISCO_IDP):
345+
context.state[Context.KEY_MEMORIZED_DISCO_IDP] = entity_id
351346

352347
return self.authn_request(context, entity_id)
353348

src/satosa/base.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,9 @@ def _auth_resp_finish(self, context, internal_response):
161161
self.config.get("USER_ID_HASH_SALT", ""),
162162
)
163163

164-
# remove all session state unless CONTEXT_STATE_DELETE is false
164+
# remove all session state unless CONTEXT_STATE_DELETE is False
165+
context.state.delete = self.config.get("CONTEXT_STATE_DELETE", True)
165166
context.request = None
166-
if "CONTEXT_STATE_DELETE" in self.config:
167-
context.state.delete = self.config["CONTEXT_STATE_DELETE"]
168-
else:
169-
context.state.delete = True
170167

171168
frontend = self.module_router.frontend_routing(context)
172169
return frontend.handle_authn_response(context, internal_response)

src/satosa/context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Context(object):
1818
KEY_BACKEND_METADATA_STORE = 'metadata_store'
1919
KEY_TARGET_ENTITYID = 'target_entity_id'
2020
KEY_FORCE_AUTHN = 'force_authn'
21+
KEY_MEMORIZED_DISCO_IDP = 'memorized_disco_idp'
2122

2223
def __init__(self):
2324
self._path = None

src/satosa/frontends/saml2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ def _handle_authn_request(self, context, binding_in, idp):
192192
authn_req = req_info.message
193193
satosa_logging(logger, logging.DEBUG, "%s" % authn_req, context.state)
194194

195+
# keep the ForceAuthn value to be used by plugins
195196
context.decorate(Context.KEY_FORCE_AUTHN, authn_req.force_authn)
196197

197198
try:

0 commit comments

Comments
 (0)