-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support LOA claims and claim processing #72
Merged
sergei-maertens
merged 11 commits into
master
from
feature/loa-claims-and-value-mapping
Jun 24, 2024
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
8e4af3b
:recycle: Rename abstract base model
sergei-maertens 568b730
:card_file_box: Add configuration fields for LOA
sergei-maertens bf41242
:card_file_box: Add LOA mapping configuration field
sergei-maertens 40f5fce
:sparkles: Implement claim processing for DigiDConfig
sergei-maertens c24df7d
:white_check_mark: Add tests for DigiD Machtigen claim processing
sergei-maertens dc2fe04
:white_check_mark: Add tests for eherkenning claim processing
sergei-maertens 0a32e31
:white_check_mark: Tests for eh-bewindvoering claim processing
sergei-maertens 0e6e299
:pencil: Document OIDC claim processing helper
sergei-maertens 4babb67
:pencil: Document new features in changelog
sergei-maertens 2495107
:sparkles: Handle missing loa_claim configuration
sergei-maertens 0b17cfe
:sparkles: Add strict/lax mode for claim processing
sergei-maertens File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import logging | ||
|
||
from glom import Path, PathAccessError, glom | ||
from mozilla_django_oidc_db.typing import ClaimPath, JSONObject | ||
|
||
from .models import BaseConfig | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class NoLOAClaim(Exception): | ||
pass | ||
|
||
|
||
def process_claims( | ||
claims: JSONObject, | ||
config: BaseConfig, | ||
strict: bool = True, | ||
) -> JSONObject: | ||
""" | ||
Given the raw claims, process them using the provided config. | ||
|
||
Claim processing performs the following steps: | ||
|
||
* Claim name normalization, the provided config model field names are used as keys | ||
* Extracting required and optional values. An error is thrown for missing required | ||
claims, unless a default value is specified in the config. | ||
* Claim value post-processing - if values need to be translated/normalized, the | ||
provided configuration is used. | ||
|
||
The return value SHOULD include the ``loa_claim`` key, but if no value is available | ||
(not in the claims and no default specified -> then it's omitted), the key will be | ||
absent. | ||
|
||
:arg claims: The raw claims as received from the Identity Provider. | ||
:arg config: The OIDC Configuration instance that specifies which claims should be | ||
extracted and processed. | ||
:arg strict: In strict mode, absent claims that are required (according) to the | ||
configuration raise an error. In non-strict mode, these claims are simply skipped | ||
and omitted. | ||
:returns: A (JSON-serializable) dictionary where the keys are the claim config | ||
field names, taken from ``config.CLAIMS_CONFIGURATION``, and the values their | ||
extracted values from the raw claims. Extracted values have been post-processed | ||
if post-processing configuration was available. | ||
""" | ||
processed_claims = {} | ||
|
||
# first, extract all the configured required claims | ||
for claim_config in config.CLAIMS_CONFIGURATION: | ||
field_name = claim_config["field"] | ||
path_bits: ClaimPath = getattr(config, field_name) | ||
try: | ||
value = glom(claims, Path(*path_bits)) | ||
except PathAccessError as exc: | ||
if not claim_config["required"]: | ||
continue | ||
# in non-strict mode, do not raise but instead omit the claim. Up to the | ||
# caller to handle missing claims. | ||
if not strict: | ||
continue | ||
claim_repr = " > ".join(path_bits) | ||
raise ValueError(f"Required claim '{claim_repr}' not found") from exc | ||
|
||
processed_claims[field_name] = value | ||
|
||
# then, loa is hardcoded in the base model, process those... | ||
try: | ||
loa = _process_loa(claims, config) | ||
except NoLOAClaim as exc: | ||
logger.info( | ||
"Missing LoA claim, excluding it from processed claims", exc_info=exc | ||
) | ||
else: | ||
processed_claims["loa_claim"] = loa | ||
|
||
return processed_claims | ||
|
||
|
||
def _process_loa(claims: JSONObject, config: BaseConfig) -> str: | ||
default = config.default_loa | ||
if not (loa_claim := config.loa_claim) and not default: | ||
raise NoLOAClaim("No LoA claim or default LoA configured") | ||
|
||
if not loa_claim: | ||
return default | ||
|
||
try: | ||
loa = glom(claims, Path(*config.loa_claim)) | ||
loa_claim_missing = False | ||
except PathAccessError: | ||
# default could be empty (string)! | ||
loa = default | ||
loa_claim_missing = not default | ||
|
||
if loa_claim_missing: | ||
raise NoLOAClaim("LoA claim is absent and no default LoA configured") | ||
|
||
# 'from' is string or number, which are valid keys | ||
loa_map: dict[str | float | int, str] = { | ||
mapping["from"]: mapping["to"] for mapping in config.loa_value_mapping | ||
} | ||
|
||
# apply mapping, if not found -> use the literal original value instead | ||
return loa_map.get(loa, loa) |
168 changes: 168 additions & 0 deletions
168
...herkenning/oidc/migrations/0007_digidconfig_default_loa_digidconfig_loa_claim_and_more.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
# Generated by Django 4.2.13 on 2024-06-21 10:17 | ||
|
||
from django.db import migrations, models | ||
|
||
import mozilla_django_oidc_db.fields | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
( | ||
"digid_eherkenning_oidc_generics", | ||
"0006_alter_digidconfig_oidc_rp_scopes_list_and_more", | ||
), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name="digidconfig", | ||
name="default_loa", | ||
field=models.CharField( | ||
blank=True, | ||
choices=[ | ||
( | ||
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", | ||
"DigiD Basis", | ||
), | ||
( | ||
"urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract", | ||
"DigiD Midden", | ||
), | ||
( | ||
"urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard", | ||
"DigiD Substantieel", | ||
), | ||
( | ||
"urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI", | ||
"DigiD Hoog", | ||
), | ||
], | ||
help_text="Fallback level of assurance, in case no claim value could be extracted.", | ||
max_length=100, | ||
verbose_name="default LOA", | ||
), | ||
), | ||
migrations.AddField( | ||
model_name="digidconfig", | ||
name="loa_claim", | ||
field=mozilla_django_oidc_db.fields.ClaimField( | ||
base_field=models.CharField( | ||
max_length=50, verbose_name="claim path segment" | ||
), | ||
blank=True, | ||
default=None, | ||
help_text="Name of the claim holding the level of assurance. If left empty, it is assumed there is no LOA claim and the configured callback value will be used.", | ||
null=True, | ||
size=None, | ||
verbose_name="LoA claim", | ||
), | ||
), | ||
migrations.AddField( | ||
model_name="digidmachtigenconfig", | ||
name="default_loa", | ||
field=models.CharField( | ||
blank=True, | ||
choices=[ | ||
( | ||
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", | ||
"DigiD Basis", | ||
), | ||
( | ||
"urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract", | ||
"DigiD Midden", | ||
), | ||
( | ||
"urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard", | ||
"DigiD Substantieel", | ||
), | ||
( | ||
"urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI", | ||
"DigiD Hoog", | ||
), | ||
], | ||
help_text="Fallback level of assurance, in case no claim value could be extracted.", | ||
max_length=100, | ||
verbose_name="default LOA", | ||
), | ||
), | ||
migrations.AddField( | ||
model_name="digidmachtigenconfig", | ||
name="loa_claim", | ||
field=mozilla_django_oidc_db.fields.ClaimField( | ||
base_field=models.CharField( | ||
max_length=50, verbose_name="claim path segment" | ||
), | ||
blank=True, | ||
default=None, | ||
help_text="Name of the claim holding the level of assurance. If left empty, it is assumed there is no LOA claim and the configured callback value will be used.", | ||
null=True, | ||
size=None, | ||
verbose_name="LoA claim", | ||
), | ||
), | ||
migrations.AddField( | ||
model_name="eherkenningbewindvoeringconfig", | ||
name="default_loa", | ||
field=models.CharField( | ||
blank=True, | ||
choices=[ | ||
("urn:etoegang:core:assurance-class:loa1", "Non existent (1)"), | ||
("urn:etoegang:core:assurance-class:loa2", "Low (2)"), | ||
("urn:etoegang:core:assurance-class:loa2plus", "Low (2+)"), | ||
("urn:etoegang:core:assurance-class:loa3", "Substantial (3)"), | ||
("urn:etoegang:core:assurance-class:loa4", "High (4)"), | ||
], | ||
help_text="Fallback level of assurance, in case no claim value could be extracted.", | ||
max_length=100, | ||
verbose_name="default LOA", | ||
), | ||
), | ||
migrations.AddField( | ||
model_name="eherkenningbewindvoeringconfig", | ||
name="loa_claim", | ||
field=mozilla_django_oidc_db.fields.ClaimField( | ||
base_field=models.CharField( | ||
max_length=50, verbose_name="claim path segment" | ||
), | ||
blank=True, | ||
default=None, | ||
help_text="Name of the claim holding the level of assurance. If left empty, it is assumed there is no LOA claim and the configured callback value will be used.", | ||
null=True, | ||
size=None, | ||
verbose_name="LoA claim", | ||
), | ||
), | ||
migrations.AddField( | ||
model_name="eherkenningconfig", | ||
name="default_loa", | ||
field=models.CharField( | ||
blank=True, | ||
choices=[ | ||
("urn:etoegang:core:assurance-class:loa1", "Non existent (1)"), | ||
("urn:etoegang:core:assurance-class:loa2", "Low (2)"), | ||
("urn:etoegang:core:assurance-class:loa2plus", "Low (2+)"), | ||
("urn:etoegang:core:assurance-class:loa3", "Substantial (3)"), | ||
("urn:etoegang:core:assurance-class:loa4", "High (4)"), | ||
], | ||
help_text="Fallback level of assurance, in case no claim value could be extracted.", | ||
max_length=100, | ||
verbose_name="default LOA", | ||
), | ||
), | ||
migrations.AddField( | ||
model_name="eherkenningconfig", | ||
name="loa_claim", | ||
field=mozilla_django_oidc_db.fields.ClaimField( | ||
base_field=models.CharField( | ||
max_length=50, verbose_name="claim path segment" | ||
), | ||
blank=True, | ||
default=None, | ||
help_text="Name of the claim holding the level of assurance. If left empty, it is assumed there is no LOA claim and the configured callback value will be used.", | ||
null=True, | ||
size=None, | ||
verbose_name="LoA claim", | ||
), | ||
), | ||
] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not familiar with the types of response we get from IdPs, but is the reason we don't just do the following here:
Because the
loa
retrieved from the claims can be""
and that is valid?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Signicat appears to only return the BSN and no LOA information, Anoigo returns strings like
"20"
but I'm being very defensive here since other providers may do even other things or intermediate keycloaks can rewrite claims...Basically we check for us if a default is set to fall back: we consider the claim missing if:
in other cases we can return a value:
0
or whatever to a value that makes sense