Skip to content

Commit

Permalink
Add dynamic requested attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
nsklikas committed Jul 1, 2020
1 parent 74fc79a commit 4d06f55
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 15 deletions.
24 changes: 24 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,30 @@ config:
[...]
```

##### Dynamic requested attributes

The `dynamic_requested_attributes` option can be used to enable the requested
attributes eIDAS extension for requesting attributes from the IdP. These
attributes are populated dynamically using the attributes which were
requested from the frontend.

In order for this to work the frontend must populate the internal request's
`attributes` field.

To enable this feature we need to provide a list of the friendly names of the
attributes which we want to be able to request and whether they are required or
not. E.g.:

```yaml
config:
dynamic_requested_attributes:
- friendly_name: attr1
required: True
- friendly_name: attr2
required: False
[...]
```

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

#### Backend
Expand Down
35 changes: 35 additions & 0 deletions src/satosa/attribute_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,38 @@ def from_internal(self, attribute_profile, internal_dict):
external_dict[external_attribute_name] = internal_dict[internal_attribute_name]

return external_dict

def from_internal_filter(
self, attribute_profile, internal_attribute_names
):
"""
Converts attribute names from internal to external "type"
:type attribute_profile: str
:type internal_attribute_names: list[str]
:rtype: list[str]
:param attribute_profile: To which external type to convert to
(ex: oidc, saml, ...)
:param internal_attribute_names: A list of attribute names
:return: A list of attribute names in the external format
"""
external_attribute_names = set()
for internal_attribute_name in internal_attribute_names:
try:
external_attribute_name = self.from_internal_attributes[
internal_attribute_name
]
# Take the first value always
external_attribute_names.add(
external_attribute_name[attribute_profile][0]
)
except KeyError:
logger.warn(
f"No attribute mapping found for the attribute "
f"{internal_attribute_name} to the profile "
f"{attribute_profile}"
)
pass

return list(external_attribute_names)
85 changes: 70 additions & 15 deletions src/satosa/backends/saml2.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class SAMLBackend(BackendModule, SAMLBaseModule):
KEY_MIRROR_FORCE_AUTHN = 'mirror_force_authn'
KEY_MEMORIZE_IDP = 'memorize_idp'
KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN = 'use_memorized_idp_when_force_authn'
KEY_DYNAMIC_REQUESTED_ATTRIBUTES = 'dynamic_requested_attributes'

VALUE_ACR_COMPARISON_DEFAULT = 'exact'

Expand Down Expand Up @@ -113,6 +114,9 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name):
self.encryption_keys = []
self.outstanding_queries = {}
self.idp_blacklist_file = config.get('idp_blacklist_file', None)
self.requested_attributes = self.config.get(
SAMLBackend.KEY_DYNAMIC_REQUESTED_ATTRIBUTES
)

sp_keypairs = sp_config.getattr('encryption_keypairs', '')
sp_key_file = sp_config.getattr('key_file', '')
Expand Down Expand Up @@ -170,15 +174,22 @@ def start_auth(self, context, internal_req):
"""

entity_id = self.get_idp_entity_id(context)
requested_attributes = internal_req.get("attributes")
if entity_id is None:
# since context is not passed to disco_query
# keep the information in the state cookie
context.state[Context.KEY_FORCE_AUTHN] = get_force_authn(
context, self.config, self.sp.config
)
if self.config.get(SAMLBackend.KEY_DYNAMIC_REQUESTED_ATTRIBUTES):
# We need the requested attributes, so store them in the cookie
context.state[Context.KEY_REQUESTED_ATTRIBUTES] = \
requested_attributes
return self.disco_query(context)

return self.authn_request(context, entity_id)
return self.authn_request(
context, entity_id, requested_attributes=requested_attributes
)

def disco_query(self, context):
"""
Expand Down Expand Up @@ -232,13 +243,59 @@ def construct_requested_authn_context(self, entity_id):

return authn_context

def authn_request(self, context, entity_id):
def _get_requested_attributes(self, requested_attributes):
if not requested_attributes:
return

attrs = self.converter.from_internal_filter(
self.attribute_profile, requested_attributes
)
requested_attrs = []
for attr in attrs:
# Internal attributes map to the attribute's friendly_name
for req_attr in self.requested_attributes:
if req_attr['friendly_name'] == attr:
requested_attrs.append(
dict(
friendly_name=attr,
required=req_attr['required']
)
)

return requested_attrs

def _get_authn_request_args(
self, context, entity_id, requested_attributes=None
):
kwargs = {}
authn_context = self.construct_requested_authn_context(entity_id)
_, response_binding = self.sp.config.getattr(
"endpoints", "sp"
)["assertion_consumer_service"][0]
kwargs["binding"] = response_binding

if authn_context:
kwargs["requested_authn_context"] = authn_context
if self.config.get(SAMLBackend.KEY_MIRROR_FORCE_AUTHN):
kwargs["force_authn"] = get_force_authn(
context, self.config, self.sp.config
)
if self.requested_attributes:
requested_attributes = self._get_requested_attributes(
requested_attributes
)
if requested_attributes:
kwargs["requested_attributes"] = requested_attributes
return kwargs

def authn_request(self, context, entity_id, requested_attributes=None):
"""
Do an authorization request on idp with given entity id.
This is the start of the authorization.
:type context: satosa.context.Context
:type entity_id: str
:type requested_attributes: list
:rtype: satosa.response.Response
:param context: The current context
Expand All @@ -257,15 +314,6 @@ def authn_request(self, context, entity_id):
logger.debug(logline, exc_info=False)
raise SATOSAAuthenticationError(context.state, "Selected IdP is blacklisted for this backend")

kwargs = {}
authn_context = self.construct_requested_authn_context(entity_id)
if authn_context:
kwargs["requested_authn_context"] = authn_context
if self.config.get(SAMLBackend.KEY_MIRROR_FORCE_AUTHN):
kwargs["force_authn"] = get_force_authn(
context, self.config, self.sp.config
)

try:
binding, destination = self.sp.pick_binding(
"single_sign_on_service", None, "idpsso", entity_id=entity_id
Expand All @@ -274,10 +322,10 @@ def authn_request(self, context, entity_id):
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
logger.debug(logline)

acs_endp, response_binding = self.sp.config.getattr("endpoints", "sp")["assertion_consumer_service"][0]
req_id, req = self.sp.create_authn_request(
destination, binding=response_binding, **kwargs
kwargs = self._get_authn_request_args(
context, entity_id, requested_attributes=requested_attributes
)
req_id, req = self.sp.create_authn_request(destination, **kwargs)
relay_state = util.rndstr()
ht_args = self.sp.apply_binding(binding, "%s" % req, destination, relay_state=relay_state)
msg = "ht_args: {}".format(ht_args)
Expand Down Expand Up @@ -363,6 +411,9 @@ def disco_response(self, context):
"""
info = context.request
state = context.state
requested_attributes = state.pop(
Context.KEY_REQUESTED_ATTRIBUTES, None
)

try:
entity_id = info["entityID"]
Expand All @@ -372,7 +423,11 @@ def disco_response(self, context):
logger.debug(logline, exc_info=True)
raise SATOSAAuthenticationError(state, "No IDP chosen") from err

return self.authn_request(context, entity_id)
return self.authn_request(
context,
entity_id,
requested_attributes=requested_attributes
)

def _translate_response(self, response, state):
"""
Expand Down
1 change: 1 addition & 0 deletions src/satosa/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Context(object):
KEY_TARGET_ENTITYID = 'target_entity_id'
KEY_FORCE_AUTHN = 'force_authn'
KEY_MEMORIZED_IDP = 'memorized_idp'
KEY_REQUESTED_ATTRIBUTES = 'requested_attributes'
KEY_AUTHN_CONTEXT_CLASS_REF = 'authn_context_class_ref'

def __init__(self):
Expand Down

0 comments on commit 4d06f55

Please sign in to comment.