35
35
logger = logging .getLogger (__name__ )
36
36
37
37
38
+ def get_memorized_idp (context , config , force_authn ):
39
+ memorized_idp = (
40
+ config .get (SAMLBackend .KEY_MEMORIZE_IDP )
41
+ and context .state .get (Context .KEY_MEMORIZED_IDP )
42
+ )
43
+ use_when_force_authn = config .get (
44
+ SAMLBackend .KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN
45
+ )
46
+ value = (not force_authn or use_when_force_authn ) and memorized_idp
47
+ return value
48
+
49
+
50
+ def get_force_authn (context , config , sp_config ):
51
+ """
52
+ Return the force_authn value.
53
+
54
+ The value comes from one of three place:
55
+ - the configuration of the backend
56
+ - the context, as it came through in the AuthnRequest handled by the frontend.
57
+ note: the frontend should have been set to mirror the force_authn value.
58
+ - the cookie, as it has been stored by the proxy on a redirect to the DS
59
+ note: the frontend should have been set to mirror the force_authn value.
60
+
61
+ The value is either "true" or False
62
+ """
63
+ mirror = config .get (SAMLBackend .KEY_MIRROR_FORCE_AUTHN )
64
+ from_state = mirror and context .state .get (Context .KEY_FORCE_AUTHN )
65
+ from_context = mirror and context .get_decoration (Context .KEY_FORCE_AUTHN )
66
+ from_config = sp_config .getattr ("force_authn" , "sp" )
67
+ is_set = str (from_state or from_context or from_config ).lower () == "true"
68
+ value = is_set and "true"
69
+ return value
70
+
71
+
38
72
class SAMLBackend (BackendModule , SAMLBaseModule ):
39
73
"""
40
74
A saml2 backend module (acting as a SP).
@@ -43,6 +77,10 @@ class SAMLBackend(BackendModule, SAMLBaseModule):
43
77
KEY_SAML_DISCOVERY_SERVICE_URL = 'saml_discovery_service_url'
44
78
KEY_SAML_DISCOVERY_SERVICE_POLICY = 'saml_discovery_service_policy'
45
79
KEY_SP_CONFIG = 'sp_config'
80
+ KEY_MIRROR_FORCE_AUTHN = 'mirror_force_authn'
81
+ KEY_MEMORIZE_IDP = 'memorize_idp'
82
+ KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN = 'use_memorized_idp_when_force_authn'
83
+
46
84
VALUE_ACR_COMPARISON_DEFAULT = 'exact'
47
85
48
86
def __init__ (self , outgoing , internal_attributes , config , base_url , name ):
@@ -64,10 +102,12 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name):
64
102
super ().__init__ (outgoing , internal_attributes , base_url , name )
65
103
self .config = self .init_config (config )
66
104
67
- sp_config = SPConfig ().load (copy .deepcopy (config [self .KEY_SP_CONFIG ]), False )
105
+ sp_config = SPConfig ().load (copy .deepcopy (
106
+ config [SAMLBackend .KEY_SP_CONFIG ]), False
107
+ )
68
108
self .sp = Base (sp_config )
69
109
70
- self .discosrv = config .get (self .KEY_DISCO_SRV )
110
+ self .discosrv = config .get (SAMLBackend .KEY_DISCO_SRV )
71
111
self .encryption_keys = []
72
112
self .outstanding_queries = {}
73
113
self .idp_blacklist_file = config .get ('idp_blacklist_file' , None )
@@ -85,26 +125,60 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name):
85
125
with open (p ) as key_file :
86
126
self .encryption_keys .append (key_file .read ())
87
127
128
+ def get_idp_entity_id (self , context ):
129
+ """
130
+ :type context: satosa.context.Context
131
+ :rtype: str | None
132
+
133
+ :param context: The current context
134
+ :return: the entity_id of the idp or None
135
+ """
136
+
137
+ idps = self .sp .metadata .identity_providers ()
138
+ only_one_idp_in_metadata = (
139
+ "mdq" not in self .config ["sp_config" ]["metadata" ]
140
+ and len (idps ) == 1
141
+ )
142
+
143
+ only_idp = only_one_idp_in_metadata and idps [0 ]
144
+ target_entity_id = context .get_decoration (Context .KEY_TARGET_ENTITYID )
145
+ force_authn = get_force_authn (context , self .config , self .sp .config )
146
+ memorized_idp = get_memorized_idp (context , self .config , force_authn )
147
+ entity_id = only_idp or target_entity_id or memorized_idp or None
148
+
149
+ satosa_logging (
150
+ logger , logging .INFO ,
151
+ {
152
+ "message" : "Selected IdP" ,
153
+ "only_one" : only_idp ,
154
+ "target_entity_id" : target_entity_id ,
155
+ "force_authn" : force_authn ,
156
+ "memorized_idp" : memorized_idp ,
157
+ "entity_id" : entity_id ,
158
+ },
159
+ context .state ,
160
+ )
161
+ return entity_id
162
+
88
163
def start_auth (self , context , internal_req ):
89
164
"""
90
165
See super class method satosa.backends.base.BackendModule#start_auth
166
+
91
167
:type context: satosa.context.Context
92
168
:type internal_req: satosa.internal.InternalData
93
169
:rtype: satosa.response.Response
94
170
"""
95
171
96
- target_entity_id = context .get_decoration (Context .KEY_TARGET_ENTITYID )
97
- if target_entity_id :
98
- entity_id = target_entity_id
99
- return self .authn_request (context , entity_id )
100
-
101
- # if there is only one IdP in the metadata, bypass the discovery service
102
- idps = self .sp .metadata .identity_providers ()
103
- if len (idps ) == 1 and "mdq" not in self .config ["sp_config" ]["metadata" ]:
104
- entity_id = idps [0 ]
105
- return self .authn_request (context , entity_id )
172
+ entity_id = self .get_idp_entity_id (context )
173
+ if entity_id is None :
174
+ # since context is not passed to disco_query
175
+ # keep the information in the state cookie
176
+ context .state [Context .KEY_FORCE_AUTHN ] = get_force_authn (
177
+ context , self .config , self .sp .config
178
+ )
179
+ return self .disco_query (context )
106
180
107
- return self .disco_query (context )
181
+ return self .authn_request (context , entity_id )
108
182
109
183
def disco_query (self , context ):
110
184
"""
@@ -122,9 +196,12 @@ def disco_query(self, context):
122
196
return_url = endpoints ["discovery_response" ][0 ][0 ]
123
197
124
198
disco_url = (
125
- context .get_decoration (self .KEY_SAML_DISCOVERY_SERVICE_URL ) or self .discosrv
199
+ context .get_decoration (SAMLBackend .KEY_SAML_DISCOVERY_SERVICE_URL )
200
+ or self .discosrv
201
+ )
202
+ disco_policy = context .get_decoration (
203
+ SAMLBackend .KEY_SAML_DISCOVERY_SERVICE_POLICY
126
204
)
127
- disco_policy = context .get_decoration (self .KEY_SAML_DISCOVERY_SERVICE_POLICY )
128
205
129
206
args = {"return" : return_url }
130
207
if disco_policy :
@@ -181,7 +258,11 @@ def authn_request(self, context, entity_id):
181
258
kwargs = {}
182
259
authn_context = self .construct_requested_authn_context (entity_id )
183
260
if authn_context :
184
- kwargs ['requested_authn_context' ] = authn_context
261
+ kwargs ["requested_authn_context" ] = authn_context
262
+ if self .config .get (SAMLBackend .KEY_MIRROR_FORCE_AUTHN ):
263
+ kwargs ["force_authn" ] = get_force_authn (
264
+ context , self .config , self .sp .config
265
+ )
185
266
186
267
try :
187
268
binding , destination = self .sp .pick_binding (
@@ -248,8 +329,11 @@ def authn_response(self, context, binding):
248
329
raise SATOSAAuthenticationError (context .state , "State did not match relay state" )
249
330
250
331
context .decorate (Context .KEY_BACKEND_METADATA_STORE , self .sp .metadata )
251
-
252
- del context .state [self .name ]
332
+ if self .config .get (SAMLBackend .KEY_MEMORIZE_IDP ):
333
+ issuer = authn_response .response .issuer .text .strip ()
334
+ context .state [Context .KEY_MEMORIZED_IDP ] = issuer
335
+ context .state .pop (self .name , None )
336
+ context .state .pop (Context .KEY_FORCE_AUTHN , None )
253
337
return self .auth_callback_func (context , self ._translate_response (authn_response , context .state ))
254
338
255
339
def disco_response (self , context ):
0 commit comments