Skip to content

Commit

Permalink
Merge pull request #64 from maykinmedia/feature/make-eidas-configurable
Browse files Browse the repository at this point in the history
Make eIDAS LoA configurable
  • Loading branch information
SilviaAmAm authored Mar 21, 2024
2 parents 0985701 + 79825e8 commit 57931d4
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 38 deletions.
73 changes: 73 additions & 0 deletions digid_eherkenning/migrations/0008_update_loa_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Generated by Django 4.2.10 on 2024-03-08 08:45

from django.db import migrations, models

import digid_eherkenning.choices


class Migration(migrations.Migration):

dependencies = [
(
"digid_eherkenning",
"0007_eherkenningconfiguration_service_description_url",
),
]

operations = [
migrations.RemoveConstraint(
model_name="eherkenningconfiguration",
name="valid_loa",
),
migrations.RenameField(
model_name="eherkenningconfiguration",
old_name="loa",
new_name="eh_loa",
),
migrations.AlterField(
model_name="eherkenningconfiguration",
name="eh_loa",
field=models.CharField(
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)"),
],
default="urn:etoegang:core:assurance-class:loa3",
help_text="Level of Assurance (LoA) to use for the eHerkenning service.",
max_length=100,
verbose_name="eHerkenning LoA",
),
),
migrations.AddField(
model_name="eherkenningconfiguration",
name="eidas_loa",
field=models.CharField(
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)"),
],
default="urn:etoegang:core:assurance-class:loa3",
help_text="Level of Assurance (LoA) to use for the eIDAS service.",
max_length=100,
verbose_name="eIDAS LoA",
),
),
migrations.AddConstraint(
model_name="eherkenningconfiguration",
constraint=models.CheckConstraint(
check=models.Q(
models.Q(
("eh_loa__in", digid_eherkenning.choices.AssuranceLevels),
("eidas_loa__in", digid_eherkenning.choices.AssuranceLevels),
)
),
name="valid_loa",
),
),
]
25 changes: 19 additions & 6 deletions digid_eherkenning/models/eherkenning.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.utils.translation import gettext_lazy as _

from ..choices import AssuranceLevels
from ..types import EHerkenningConfig
from ..validators import oin_validator
from .base import BaseConfiguration

Expand Down Expand Up @@ -60,11 +61,11 @@ def get_default_requested_attributes_eidas():


class EherkenningConfiguration(BaseConfiguration):
loa = models.CharField(
_("LoA"),
eh_loa = models.CharField(
_("eHerkenning LoA"),
choices=AssuranceLevels.choices,
default=AssuranceLevels.substantial,
help_text=_("Level of Assurance (LoA) to use for all the services."),
help_text=_("Level of Assurance (LoA) to use for the eHerkenning service."),
max_length=100,
)
eh_attribute_consuming_service_index = models.CharField(
Expand Down Expand Up @@ -99,6 +100,13 @@ class EherkenningConfiguration(BaseConfiguration):
"changing the value is a manual process."
),
)
eidas_loa = models.CharField(
_("eIDAS LoA"),
choices=AssuranceLevels.choices,
default=AssuranceLevels.substantial,
help_text=_("Level of Assurance (LoA) to use for the eIDAS service."),
max_length=100,
)
eidas_attribute_consuming_service_index = models.CharField(
_("eIDAS attribute consuming service index"),
blank=True,
Expand Down Expand Up @@ -176,11 +184,15 @@ class Meta:
verbose_name = _("Eherkenning/eIDAS configuration")
constraints = [
models.constraints.CheckConstraint(
name="valid_loa", check=models.Q(loa__in=AssuranceLevels)
name="valid_loa",
check=models.Q(
models.Q(eh_loa__in=AssuranceLevels)
& models.Q(eidas_loa__in=AssuranceLevels)
),
),
]

def as_dict(self) -> dict:
def as_dict(self) -> EHerkenningConfig:
"""
Emit the configuration as a dictionary compatible with the old settings format.
"""
Expand Down Expand Up @@ -215,6 +227,7 @@ def as_dict(self) -> dict:
"service_description": self.service_description,
"service_description_url": self.service_description_url,
"service_url": self.base_url,
"loa": self.eh_loa,
"privacy_policy_url": self.privacy_policy,
"herkenningsmakelaars_id": self.makelaar_id,
"requested_attributes": self.eh_requested_attributes,
Expand Down Expand Up @@ -247,6 +260,7 @@ def as_dict(self) -> dict:
"service_description": self.service_description,
"service_description_url": self.service_description_url,
"service_url": self.base_url,
"loa": self.eidas_loa,
"privacy_policy_url": self.privacy_policy,
"herkenningsmakelaars_id": self.makelaar_id,
"requested_attributes": self.eidas_requested_attributes,
Expand All @@ -270,7 +284,6 @@ def as_dict(self) -> dict:
"service_entity_id": self.idp_service_entity_id,
"oin": self.oin,
"services": services,
"loa": self.loa,
# optional in runtime code
"want_assertions_encrypted": self.want_assertions_encrypted,
"want_assertions_signed": self.want_assertions_signed,
Expand Down
32 changes: 17 additions & 15 deletions digid_eherkenning/saml2/eherkenning.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import binascii
from base64 import b64encode
from io import BytesIO
from typing import List, Literal, Union
from typing import Literal, Union
from uuid import uuid4

from django.urls import reverse
Expand All @@ -12,10 +12,12 @@
from furl.furl import furl
from lxml.builder import ElementMaker
from lxml.etree import Element, tostring
from onelogin.saml2.settings import OneLogin_Saml2_Settings

from ..choices import AssuranceLevels
from ..models import EherkenningConfiguration
from ..settings import EHERKENNING_DS_XSD
from ..types import EHerkenningConfig, EHerkenningSAMLConfig, ServiceConfig
from ..utils import validate_xml
from .base import BaseSaml2Client, get_service_description, get_service_name

Expand All @@ -36,7 +38,7 @@

def generate_dienst_catalogus_metadata(eherkenning_config=None):
eherkenning_config = eherkenning_config or EherkenningConfiguration.get_solo()
settings = eherkenning_config.as_dict()
settings: EHerkenningConfig = eherkenning_config.as_dict()
# ensure that the single language strings are output in both nl and en
for service in settings["services"]:
name = service["service_name"]
Expand Down Expand Up @@ -295,7 +297,7 @@ def create_classifiers_element(classifiers: list) -> ElementMaker:
return ESC("Classifiers", *classifiers_elements)


def create_key_descriptor(x509_certificate_content: bytes):
def create_key_descriptor(x509_certificate_content: bytes) -> ElementMaker:
certificate = load_pem_x509_certificate(x509_certificate_content)
key_name = binascii.hexlify(
certificate.fingerprint(certificate.signature_hash_algorithm)
Expand All @@ -317,7 +319,7 @@ def create_key_descriptor(x509_certificate_content: bytes):
return MD("KeyDescriptor", *args, **kwargs)


def create_service_catalogus(conf, validate=True):
def create_service_catalogus(conf: EHerkenningConfig, validate: bool = True) -> bytes:
"""
https://afsprakenstelsel.etoegang.nl/display/as/Service+catalog
"""
Expand Down Expand Up @@ -366,7 +368,7 @@ def create_service_catalogus(conf, validate=True):
service_description,
service_description_url,
# https://afsprakenstelsel.etoegang.nl/display/as/Level+of+assurance
conf["loa"],
service["loa"],
entity_concerned_types_allowed,
requested_attributes,
herkenningsmakelaars_id,
Expand Down Expand Up @@ -403,8 +405,8 @@ def create_service_catalogus(conf, validate=True):


def get_metadata_eherkenning_requested_attributes(
conf: dict, service_id: str
) -> List[dict]:
conf: ServiceConfig, service_id: str
) -> list[dict]:
# There needs to be a RequestedAttribute element where the name is the ServiceID
# https://afsprakenstelsel.etoegang.nl/display/as/DV+metadata+for+HM
requested_attributes = [{"name": service_id, "isRequired": False}]
Expand All @@ -427,7 +429,7 @@ def get_metadata_eherkenning_requested_attributes(
return requested_attributes


def create_attribute_consuming_services(conf: dict) -> list:
def create_attribute_consuming_services(conf: EHerkenningConfig) -> list[dict]:
attribute_consuming_services = []

for service in conf["services"]:
Expand Down Expand Up @@ -466,15 +468,15 @@ def __init__(
self.loa = loa

@property
def conf(self) -> dict:
def conf(self) -> EHerkenningConfig:
if not hasattr(self, "_conf"):
db_config = EherkenningConfiguration.get_solo()
self._conf = db_config.as_dict()
self._conf.setdefault("acs_path", reverse("eherkenning:acs"))
return self._conf

def create_config_dict(self, conf):
config_dict = super().create_config_dict(conf)
def create_config_dict(self, conf: EHerkenningConfig) -> EHerkenningSAMLConfig:
config_dict: EHerkenningSAMLConfig = super().create_config_dict(conf)

attribute_consuming_services = create_attribute_consuming_services(conf)
with conf["cert_file"].open("r") as cert_file, conf["key_file"].open(
Expand Down Expand Up @@ -504,7 +506,9 @@ def create_config_dict(self, conf):
)
return config_dict

def create_config(self, config_dict):
def create_config(
self, config_dict: EHerkenningSAMLConfig
) -> OneLogin_Saml2_Settings:
config_dict["security"].update(
{
# See comment in the python3-saml for in OneLogin_Saml2_Response.validate_num_assertions (onelogin/saml2/response.py)
Expand All @@ -515,9 +519,7 @@ def create_config(self, config_dict):
"metadataValidUntil": "",
"metadataCacheDuration": "",
"requestedAuthnContextComparison": "minimum",
"requestedAuthnContext": [
self.loa or self.conf["loa"],
],
"requestedAuthnContext": False if not self.loa else [self.loa],
}
)
return super().create_config(config_dict)
Expand Down
95 changes: 95 additions & 0 deletions digid_eherkenning/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from pathlib import Path
from typing import Optional, TypedDict, Union


class ServiceConfig(TypedDict):
service_uuid: str
service_name: str
attribute_consuming_service_index: str
service_instance_uuid: str
service_description: str
service_description_url: str
service_url: str
loa: str
privacy_policy_url: str
herkenningsmakelaars_id: str
requested_attributes: str
service_restrictions_allowed: str
entity_concerned_types_allowed: list[dict]
language: str
classifiers: Optional[list[str]]


class EHerkenningConfig(TypedDict):
base_url: str
acs_path: str
entity_id: str
metadata_file: str
cert_file: Path
key_file: Path
service_entity_id: str
oin: str
services: list[ServiceConfig]
want_assertions_encrypted: str
want_assertions_signed: str
key_passphrase: str
signature_algorithm: str
digest_algorithm: str
technical_contact_person_telephone: Optional[str]
technical_contact_person_email: Optional[str]
organization: str
organization_name: str
artifact_resolve_content_type: str


class ServiceProviderSAMLConfig(TypedDict):
entityId: str
assertionConsumerService: dict
singleLogoutService: dict
attributeConsumingServices: list[dict]
NameIDFormat: str
x509cert: str
privateKey: str
privateKeyPassphrase: Optional[str]


class IdentityProviderSAMLConfig(TypedDict):
entityId: str
singleSignOnService: dict
singleLogoutService: dict
x509cert: str


class SecuritySAMLConfig(TypedDict):
nameIdEncrypted: bool
authnRequestsSigned: bool
logoutRequestSigned: bool
logoutResponseSigned: bool
signMetadata: bool
wantMessagesSigned: bool
wantAssertionsSigned: bool
wantAssertionsEncrypted: bool
wantNameId: bool
wantNameIdEncrypted: bool
wantAttributeStatement: bool
requestedAuthnContext: Union[bool, list[str]]
requestedAuthnContextComparison: str
failOnAuthnContextMismatch: bool
metadataValidUntil: Optional[str]
metadataCacheDuration: Optional[str]
allowSingleLabelDomains: bool
signatureAlgorithm: str
digestAlgorithm: str
allowRepeatAttributeName: bool
rejectDeprecatedAlgorithm: bool
disableSignatureWrappingProtection: bool


class EHerkenningSAMLConfig(TypedDict):
strict: bool
debug: bool
sp: ServiceProviderSAMLConfig
idp: IdentityProviderSAMLConfig
security: SecuritySAMLConfig
contactPerson: dict
organization: dict
2 changes: 1 addition & 1 deletion digid_eherkenning/views/eherkenning.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_attribute_consuming_service_index(self) -> Optional[str]:

#
# TODO: It might be a good idea to change this to a post-verb.
# I can't think of any realy attack-vectors, but seems like a good
# I can't think of any relay attack-vectors, but seems like a good
# idea anyways.
#
def get_context_data(self, **kwargs):
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"service_name": "Example eHerkenning", # TODO: eidas variant?
"want_assertions_signed": False,
"organization_name": "Example",
"loa": "urn:etoegang:core:assurance-class:loa3",
"eh_loa": "urn:etoegang:core:assurance-class:loa3",
"eidas_loa": "urn:etoegang:core:assurance-class:loa3",
"eh_attribute_consuming_service_index": "1",
"eidas_attribute_consuming_service_index": "2",
"oin": "00000000000000000000",
Expand Down
Loading

0 comments on commit 57931d4

Please sign in to comment.