Skip to content

Commit 85ffaa3

Browse files
author
Rebecka Gulliksson
committed
Add support for configuring AuthnContextClassRef per backing provider to return from SAML frontend.
1 parent f8c02ca commit 85ffaa3

File tree

4 files changed

+120
-2
lines changed

4 files changed

+120
-2
lines changed

doc/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,30 @@ The **SamlFrontend** module acts like a regular IDP and hides the target identit
189189
The target is chosen by using a sso endpoint in the frontend IDP associated to a specific backend.
190190
It is the backend modules job to pick an identity provider.
191191

192+
##### Providing `AuthnContextClassRef`
193+
The SAML2 frontends can provide an authenication class reference in the `AuthnStatement` of the
194+
assertion in the authentication response. This can be used to describe the Level of Assurance,
195+
as described for example by [eIDAS](https://joinup.ec.europa.eu/sites/default/files/eidas_message_format_v1.0.pdf).
196+
197+
The `AuthnContextClassRef`(ACR) can be specified per backing provider in a mapping under the
198+
configuration parameter `acr_mapping`. The mapping must contain a default ACR under the key `""`
199+
(empty string), other ACR value specific per provider is specified with key-value pairs, where the
200+
key is the providers id (entity id for SAML IdP behind SAML2 backend, authorization endpoint URL for
201+
OAuth AS behind OAuth backend, and issuer for OpenID Connect OP behind OpenID Connect backend).
202+
203+
If no `acr_mapping` is provided in the configuration, the ACR from the backend plugin will
204+
be used instead. This means that when using a SAML2 backend, the ACR provided by the backing
205+
provider will preserved and passed on in the authentication response, and when using a OAuth or
206+
OpenID Connect backend, the ACR will be `urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified`.
207+
208+
**Example**
209+
210+
config:
211+
config: [...]
212+
acr_mapping:
213+
"": default-LoA
214+
"https://accounts.google.com": LoA1
215+
192216
#### Backend
193217
The SAML2 backend acts as an SAML Service Provider (SP), making authentication
194218
requests to SAML Identity Providers (IdP). The default configuration file can be

example/plugins/frontends/saml2_frontend.yaml.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ config:
2525
subject_data: ./idp.subject
2626
want_authn_requests_signed: false
2727
xmlsec_binary: /usr/bin/xmlsec1
28+
29+
acr_mapping:
30+
"": default-LoA
31+
"https://accounts.google.com": LoA1
32+
2833
state_id: <name>
2934
base: <base_url>
3035
endpoints:

src/satosa/frontends/saml2.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __init__(self, auth_req_callback_func, internal_attributes, conf):
4141
self.endpoints = conf["endpoints"]
4242
self.base = conf["base"]
4343
self.state_id = conf["state_id"]
44+
self.acr_mapping = conf.get("acr_mapping")
4445
self.response_bindings = None
4546
self.idp = None
4647

@@ -325,7 +326,12 @@ def _handle_authn_response(self, context, internal_response, idp):
325326

326327
resp_args = request_state["resp_args"]
327328
ava = self.converter.from_internal("saml", internal_response.get_attributes())
328-
auth_info = {"class_ref": internal_response.auth_info.auth_class_ref}
329+
330+
auth_info = {}
331+
if self.acr_mapping:
332+
auth_info["class_ref"] = self.acr_mapping.get(internal_response.auth_info.issuer, self.acr_mapping[""])
333+
else:
334+
auth_info["class_ref"] = internal_response.auth_info.auth_class_ref
329335

330336
name_id = NameID(text=internal_response.get_user_id(),
331337
format=get_saml_name_id_format(internal_response.user_id_hash_type),

tests/satosa/frontends/test_saml2.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
1010
from saml2.authn_context import PASSWORD
1111
from saml2.config import SPConfig
12-
from saml2.saml import NAMEID_FORMAT_PERSISTENT
12+
from saml2.saml import NAMEID_FORMAT_PERSISTENT, NAMEID_FORMAT_TRANSIENT
13+
from saml2.samlp import NameIDPolicy
1314

1415
from satosa.context import Context
1516
from satosa.frontends.saml2 import SamlFrontend
@@ -158,3 +159,85 @@ def test_get_filter_attributes_with_sp_requested_attributes_without_friendlyname
158159
assert set(filtered_attributes) == set(
159160
["edupersontargetedid", "edupersonprincipalname", "edupersonaffiliation", "mail",
160161
"displayname", "sn", "givenname"])
162+
163+
def test_acr_mapping_in_authn_response(self, idp_conf, sp_conf):
164+
eidas_loa_low = "http://eidas.europa.eu/LoA/low"
165+
loa = {"": eidas_loa_low}
166+
167+
base = self.construct_base_url_from_entity_id(idp_conf["entityid"])
168+
conf = {"idp_config": idp_conf, "endpoints": ENDPOINTS, "base": base,
169+
"state_id": "state_id", "acr_mapping": loa}
170+
171+
samlfrontend = SamlFrontend(None, INTERNAL_ATTRIBUTES, conf)
172+
samlfrontend.register_endpoints(["foo"])
173+
174+
idp_metadata_str = create_metadata_from_config_dict(samlfrontend.config)
175+
sp_conf["metadata"]["inline"].append(idp_metadata_str)
176+
fakesp = FakeSP(None, config=SPConfig().load(sp_conf, metadata_construction=False))
177+
178+
auth_info = AuthenticationInformation(PASSWORD, "2015-09-30T12:21:37Z", "unittest_idp.xml")
179+
internal_response = InternalResponse(auth_info=auth_info)
180+
context = Context()
181+
context.state = State()
182+
183+
resp_args = {
184+
"name_id_policy": NameIDPolicy(format=NAMEID_FORMAT_TRANSIENT),
185+
"in_response_to": None,
186+
"destination": "",
187+
"sp_entity_id": None,
188+
"binding": BINDING_HTTP_REDIRECT
189+
190+
}
191+
request_state = samlfrontend.save_state(context, resp_args, "")
192+
context.state.add(conf["state_id"], request_state)
193+
194+
resp = samlfrontend.handle_authn_response(context, internal_response)
195+
resp_dict = parse_qs(urlparse(resp.message).query)
196+
resp = fakesp.parse_authn_request_response(resp_dict['SAMLResponse'][0],
197+
BINDING_HTTP_REDIRECT)
198+
199+
assert len(resp.assertion.authn_statement) == 1
200+
authn_context_class_ref = resp.assertion.authn_statement[
201+
0].authn_context.authn_context_class_ref
202+
assert authn_context_class_ref.text == eidas_loa_low
203+
204+
def test_acr_mapping_per_idp_in_authn_response(self, idp_conf, sp_conf):
205+
expected_loa = "LoA1"
206+
loa = {"": "http://eidas.europa.eu/LoA/low", idp_conf["entityid"]: expected_loa}
207+
208+
base = self.construct_base_url_from_entity_id(idp_conf["entityid"])
209+
conf = {"idp_config": idp_conf, "endpoints": ENDPOINTS, "base": base,
210+
"state_id": "state_id", "acr_mapping": loa}
211+
212+
samlfrontend = SamlFrontend(None, INTERNAL_ATTRIBUTES, conf)
213+
samlfrontend.register_endpoints(["foo"])
214+
215+
idp_metadata_str = create_metadata_from_config_dict(samlfrontend.config)
216+
sp_conf["metadata"]["inline"].append(idp_metadata_str)
217+
fakesp = FakeSP(None, config=SPConfig().load(sp_conf, metadata_construction=False))
218+
219+
auth_info = AuthenticationInformation(PASSWORD, "2015-09-30T12:21:37Z", idp_conf["entityid"])
220+
internal_response = InternalResponse(auth_info=auth_info)
221+
context = Context()
222+
context.state = State()
223+
224+
resp_args = {
225+
"name_id_policy": NameIDPolicy(format=NAMEID_FORMAT_TRANSIENT),
226+
"in_response_to": None,
227+
"destination": "",
228+
"sp_entity_id": None,
229+
"binding": BINDING_HTTP_REDIRECT
230+
231+
}
232+
request_state = samlfrontend.save_state(context, resp_args, "")
233+
context.state.add(conf["state_id"], request_state)
234+
235+
resp = samlfrontend.handle_authn_response(context, internal_response)
236+
resp_dict = parse_qs(urlparse(resp.message).query)
237+
resp = fakesp.parse_authn_request_response(resp_dict['SAMLResponse'][0],
238+
BINDING_HTTP_REDIRECT)
239+
240+
assert len(resp.assertion.authn_statement) == 1
241+
authn_context_class_ref = resp.assertion.authn_statement[
242+
0].authn_context.authn_context_class_ref
243+
assert authn_context_class_ref.text == expected_loa

0 commit comments

Comments
 (0)