-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #69 from maykinmedia/feature/oidc-generics
Port OpenID Connect generics for DigiD/eHerkenning
- Loading branch information
Showing
31 changed files
with
1,991 additions
and
20 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
flag_management: | ||
default_rules: | ||
carryforward: true | ||
statuses: | ||
- type: project | ||
target: auto | ||
threshold: 1% | ||
- type: patch | ||
target: 90% | ||
individual_flags: # exceptions to the default rules above, stated flag by flag | ||
- name: base | ||
paths: | ||
- digid_eherkenning | ||
- '!digid_eherkenning/oidc/' | ||
- name: oidc | ||
paths: | ||
- digid_eherkenning/oidc/ | ||
carryforward: true |
This file was deleted.
Oops, something went wrong.
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,65 @@ | ||
""" | ||
DigiD-eHerkenning-OIDC-generics abstracts authenticating over OIDC. | ||
DigiD/eHerkenning are typically exposed via SAML bindings, but there exist identity | ||
providers that abstract this and instead offer an OpenID Connect flow to log in with | ||
DigiD and/or eHerkenning. This package facilitates integrating with such providers. | ||
The architecture and authentication flows are tricky in some places. Here's an attempt | ||
to explain it. | ||
**Configuration** | ||
Each authentication means (DigiD, eHerkenning, mandate (machtigen) variants...) is | ||
mapped to an OpenID client configuration, which roughly holds: | ||
- the OIDC endpoints to use/redirect users to | ||
- the OAUTH2 client ID and secret to use, which indicate to the IdP which authentication | ||
means they should send the user to | ||
- which claims to look up/extract from the UserInfo endpoint/JWT | ||
These are stored in (subclasses of) the | ||
:class:`~digid_herkenning.oidc.models.OpenIDConnectBaseConfig` model. | ||
**Authentication flow** | ||
When a user starts a login flow, they: | ||
1. Click the appriopriate button/link | ||
2. A Django view processes this and looks up the relevant configuration | ||
3. The view redirects the user to the identity provider (typically a different domain) | ||
4. Authenticate with the IdP | ||
5. The IdP redirects back to our application | ||
6. Our callback view performs the OIDC exchange and extracts + stores the relevant user | ||
information | ||
7. Finally, the callback view looks up where the user needs to be redirected to and | ||
sends them that way. | ||
Steps 2-3 are called the "init" phase in this package, while steps 6-7 are the | ||
"callback" phase. | ||
**Init phase** | ||
The mozilla-django-oidc-db package provides the | ||
:class:`~mozilla_django_oidc_db.views.OIDCInit` view class, for the init phase. It | ||
ensures that the specified config class is persisted in the authentication state. | ||
This package provides concrete views bound to configuration classes: | ||
* :attr:`~digid_herkenning.oidc.views.digid_init` | ||
* :attr:`~digid_herkenning.oidc.views.digid_machtigen_init` | ||
* :attr:`~digid_herkenning.oidc.views.eherkenning_init` | ||
* :attr:`~digid_herkenning.oidc.views.eherkenning_bewindvoering_init` | ||
**Callback phase** | ||
The callback phase validates the code and state, and loads which configuration class | ||
needs to be used from the state. With this information, the authentication backends | ||
from ``settings.AUTHENTICATION_BACKENDS`` are tried in order. Typically this will | ||
use the backend shipped in mozilla-django-oidc-db, or a subclass of it. | ||
The OpenID connect flow exchanges the code for an access token (and ID token), and | ||
the user details are retrieved. You should provide a customized backend to determine | ||
what needs to be done with this user information, e.g. create a django user or store | ||
the information in the django session. | ||
""" |
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,123 @@ | ||
from collections.abc import Sequence | ||
from copy import deepcopy | ||
|
||
from django.contrib import admin | ||
from django.forms import modelform_factory | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
from mozilla_django_oidc_db.forms import OpenIDConnectConfigForm | ||
from solo.admin import SingletonModelAdmin | ||
|
||
from .models import ( | ||
DigiDConfig, | ||
DigiDMachtigenConfig, | ||
EHerkenningBewindvoeringConfig, | ||
EHerkenningConfig, | ||
OpenIDConnectBaseConfig, | ||
) | ||
|
||
# Using a dict because these retain ordering, and it makes things a bit more readable. | ||
ATTRIBUTES_MAPPING_TITLE = _("Attributes to extract from claim") | ||
COMMON_FIELDSETS = { | ||
_("Activation"): { | ||
"fields": ("enabled",), | ||
}, | ||
_("Common settings"): { | ||
"fields": ( | ||
"oidc_rp_client_id", | ||
"oidc_rp_client_secret", | ||
"oidc_rp_scopes_list", | ||
"oidc_rp_sign_algo", | ||
"oidc_rp_idp_sign_key", | ||
), | ||
}, | ||
ATTRIBUTES_MAPPING_TITLE: { | ||
"fields": (), # populated from the factory function below | ||
}, | ||
_("Endpoints"): { | ||
"fields": ( | ||
"oidc_op_discovery_endpoint", | ||
"oidc_op_jwks_endpoint", | ||
"oidc_op_authorization_endpoint", | ||
"oidc_op_token_endpoint", | ||
"oidc_token_use_basic_auth", | ||
"oidc_op_user_endpoint", | ||
"oidc_op_logout_endpoint", | ||
), | ||
}, | ||
_("Keycloak specific settings"): { | ||
"fields": ("oidc_keycloak_idp_hint",), | ||
"classes": ["collapse in"], | ||
}, | ||
_("Advanced settings"): { | ||
"fields": ( | ||
"oidc_use_nonce", | ||
"oidc_nonce_size", | ||
"oidc_state_size", | ||
"oidc_exempt_urls", | ||
"userinfo_claims_source", | ||
), | ||
"classes": ["collapse in"], | ||
}, | ||
} | ||
|
||
|
||
def admin_modelform_factory(model: type[OpenIDConnectBaseConfig], *args, **kwargs): | ||
""" | ||
Factory function to generate a model form class for a given configuration model. | ||
The configuration model is expected to be a subclass of | ||
:class:`~digid_eherkenning_oidc_generics.models.OpenIDConnectBaseConfig`. | ||
Additional args and kwargs are forwarded to django's | ||
:func:`django.forms.modelform_factory`. | ||
""" | ||
kwargs.setdefault("form", OpenIDConnectConfigForm) | ||
Form = modelform_factory(model, *args, **kwargs) | ||
assert issubclass( | ||
Form, OpenIDConnectConfigForm | ||
), "The base form class must be a subclass of OpenIDConnectConfigForm." | ||
return Form | ||
|
||
|
||
def fieldsets_factory(claim_mapping_fields: Sequence[str]): | ||
""" | ||
Apply the shared fieldsets configuration with the model-specific overrides. | ||
""" | ||
_fieldsets = deepcopy(COMMON_FIELDSETS) | ||
_fieldsets[ATTRIBUTES_MAPPING_TITLE]["fields"] += tuple(claim_mapping_fields) | ||
return tuple((label, config) for label, config in _fieldsets.items()) | ||
|
||
|
||
@admin.register(DigiDConfig) | ||
class OpenIDConnectConfigDigiDAdmin(SingletonModelAdmin): | ||
form = admin_modelform_factory(DigiDConfig) | ||
fieldsets = fieldsets_factory(claim_mapping_fields=["identifier_claim_name"]) | ||
|
||
|
||
@admin.register(EHerkenningConfig) | ||
class OpenIDConnectConfigEHerkenningAdmin(SingletonModelAdmin): | ||
form = admin_modelform_factory(EHerkenningConfig) | ||
fieldsets = fieldsets_factory(claim_mapping_fields=["identifier_claim_name"]) | ||
|
||
|
||
@admin.register(DigiDMachtigenConfig) | ||
class OpenIDConnectConfigDigiDMachtigenAdmin(SingletonModelAdmin): | ||
form = admin_modelform_factory(DigiDMachtigenConfig) | ||
fieldsets = fieldsets_factory( | ||
claim_mapping_fields=[ | ||
"vertegenwoordigde_claim_name", | ||
"gemachtigde_claim_name", | ||
] | ||
) | ||
|
||
|
||
@admin.register(EHerkenningBewindvoeringConfig) | ||
class OpenIDConnectConfigEHerkenningBewindvoeringAdmin(SingletonModelAdmin): | ||
form = admin_modelform_factory(EHerkenningBewindvoeringConfig) | ||
fieldsets = fieldsets_factory( | ||
claim_mapping_fields=[ | ||
"vertegenwoordigde_company_claim_name", | ||
"gemachtigde_person_claim_name", | ||
] | ||
) |
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,10 @@ | ||
from django.apps import AppConfig | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
|
||
class OIDCAppConfig(AppConfig): | ||
name = "digid_eherkenning.oidc" | ||
verbose_name = _("DigiD & eHerkenning via OpenID Connect") | ||
# can't change this label because of existing migrations in Open Forms/Open Inwoner | ||
label = "digid_eherkenning_oidc_generics" | ||
default_auto_field = "django.db.models.AutoField" |
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,16 @@ | ||
from django.contrib.auth.models import AbstractUser | ||
|
||
from mozilla_django_oidc_db.backends import OIDCAuthenticationBackend | ||
from mozilla_django_oidc_db.typing import JSONObject | ||
|
||
from .models.base import OpenIDConnectBaseConfig | ||
|
||
|
||
class BaseBackend(OIDCAuthenticationBackend): | ||
def _check_candidate_backend(self) -> bool: | ||
suitable_model = issubclass(self.config_class, OpenIDConnectBaseConfig) | ||
return suitable_model and super()._check_candidate_backend() | ||
|
||
def update_user(self, user: AbstractUser, claims: JSONObject): | ||
# do nothing by default | ||
return user |
Oops, something went wrong.