From c3c43155fce653fb9aca14a7266eae5101a604f3 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 5 Jul 2024 23:55:51 +0200 Subject: [PATCH] Convert models to concrete models Removed the inheritance from abstract base models to simplify the dependency on mozilla_django_oidc_db, by temporarily vendoring in all the relevant model fields code. --- src/digid_eherkenning_oidc_generics/models.py | 259 +++++++++++++++--- 1 file changed, 227 insertions(+), 32 deletions(-) diff --git a/src/digid_eherkenning_oidc_generics/models.py b/src/digid_eherkenning_oidc_generics/models.py index 2b97c37a34..064ccfbcb9 100644 --- a/src/digid_eherkenning_oidc_generics/models.py +++ b/src/digid_eherkenning_oidc_generics/models.py @@ -4,6 +4,7 @@ from django_jsonform.models.fields import ArrayField from mozilla_django_oidc_db.models import OpenIDConnectConfigBase +from solo.models import SingletonModel from .digid_settings import DIGID_CUSTOM_OIDC_DB_PREFIX from .eherkenning_settings import EHERKENNING_CUSTOM_OIDC_DB_PREFIX @@ -62,35 +63,132 @@ class Meta: abstract = True -class OpenIDConnectDigiDConfig(OpenIDConnectBaseConfig): +class OpenIDConnectDigiDConfig(SingletonModel): """ Configuration for DigiD authentication via OpenID connect """ + oidc_rp_client_id = models.CharField( + help_text="OpenID Connect client ID provided by the OIDC Provider", + max_length=1000, + verbose_name="OpenID Connect client ID", + ) + oidc_rp_client_secret = models.CharField( + help_text="OpenID Connect secret provided by the OIDC Provider", + max_length=1000, + verbose_name="OpenID Connect secret", + ) + oidc_rp_sign_algo = models.CharField( + default="HS256", + help_text="Algorithm the Identity Provider uses to sign ID tokens", + max_length=50, + verbose_name="OpenID sign algorithm", + ) + oidc_op_discovery_endpoint = models.URLField( + blank=True, + help_text="URL of your OpenID Connect provider discovery endpoint ending with a slash (`.well-known/...` will be added automatically). If this is provided, the remaining endpoints can be omitted, as they will be derived from this endpoint.", + max_length=1000, + verbose_name="Discovery endpoint", + ) + oidc_op_jwks_endpoint = models.URLField( + blank=True, + help_text="URL of your OpenID Connect provider JSON Web Key Set endpoint. Required if `RS256` is used as signing algorithm.", + max_length=1000, + verbose_name="JSON Web Key Set endpoint", + ) + oidc_op_authorization_endpoint = models.URLField( + help_text="URL of your OpenID Connect provider authorization endpoint", + max_length=1000, + verbose_name="Authorization endpoint", + ) + oidc_op_token_endpoint = models.URLField( + help_text="URL of your OpenID Connect provider token endpoint", + max_length=1000, + verbose_name="Token endpoint", + ) + oidc_op_user_endpoint = models.URLField( + help_text="URL of your OpenID Connect provider userinfo endpoint", + max_length=1000, + verbose_name="User endpoint", + ) + oidc_rp_idp_sign_key = models.CharField( + blank=True, + help_text="Key the Identity Provider uses to sign ID tokens in the case of an RSA sign algorithm. Should be the signing key in PEM or DER format.", + max_length=1000, + verbose_name="Sign key", + ) + oidc_use_nonce = models.BooleanField( + default=True, + help_text="Controls whether the OpenID Connect client uses nonce verification", + verbose_name="Use nonce", + ) + oidc_nonce_size = models.PositiveIntegerField( + default=32, + help_text="Sets the length of the random string used for OpenID Connect nonce verification", + verbose_name="Nonce size", + ) + oidc_state_size = models.PositiveIntegerField( + default=32, + help_text="Sets the length of the random string used for OpenID Connect state verification", + verbose_name="State size", + ) + oidc_exempt_urls = ArrayField( + base_field=models.CharField(max_length=1000, verbose_name="Exempt URL"), + blank=True, + default=list, + help_text="This is a list of absolute url paths, regular expressions for url paths, or Django view names. This plus the mozilla-django-oidc urls are exempted from the session renewal by the SessionRefresh middleware.", + size=None, + verbose_name="URLs exempt from session renewal", + ) + userinfo_claims_source = models.CharField( + choices=[ + ("userinfo_endpoint", "Userinfo endpoint"), + ("id_token", "ID token"), + ], + default="userinfo_endpoint", + help_text="Indicates the source from which the user information claims should be extracted.", + max_length=100, + verbose_name="user information claims extracted from", + ) + oidc_op_logout_endpoint = models.URLField( + blank=True, + help_text="URL of your OpenID Connect provider logout endpoint", + max_length=1000, + verbose_name="Logout endpoint", + ) + oidc_keycloak_idp_hint = models.CharField( + blank=True, + help_text="Specific for Keycloak: parameter that indicates which identity provider should be used (therefore skipping the Keycloak login screen).", + max_length=1000, + verbose_name="Keycloak Identity Provider hint", + ) enabled = models.BooleanField( - _("enable"), default=False, - help_text=_( - "Indicates whether OpenID Connect for authentication/authorization is enabled. " - "This overrides overrides the usage of SAML for DigiD authentication." - ), + help_text="Indicates whether OpenID Connect for authentication/authorization is enabled. This overrides overrides the usage of SAML for DigiD authentication.", + verbose_name="enable", ) - identifier_claim_name = models.CharField( - _("BSN claim name"), - max_length=100, - help_text=_("The name of the claim in which the BSN of the user is stored"), default="bsn", + help_text="The name of the claim in which the BSN of the user is stored", + max_length=100, + verbose_name="BSN claim name", ) oidc_rp_scopes_list = ArrayField( - verbose_name=_("OpenID Connect scopes"), - base_field=models.CharField(_("OpenID Connect scope"), max_length=50), + base_field=models.CharField(max_length=50, verbose_name="OpenID Connect scope"), + blank=True, default=get_default_scopes_bsn, + help_text="OpenID Connect scopes that are requested during login. These scopes are hardcoded and must be supported by the identity provider", + size=None, + verbose_name="OpenID Connect scopes", + ) + + # TODO: this is not in django-digid-eherkenning + error_message_mapping = models.JSONField( blank=True, - help_text=_( - "OpenID Connect scopes that are requested during login. " - "These scopes are hardcoded and must be supported by the identity provider" - ), + default=dict, + help_text="Mapping that maps error messages returned by the identity provider to human readable error messages that are shown to the user", + max_length=1000, + verbose_name="Error message mapping", ) @classproperty @@ -102,35 +200,132 @@ class Meta: db_table = "digid_eherkenning_oidc_generics_openidconnectdigidconfig" -class OpenIDConnectEHerkenningConfig(OpenIDConnectBaseConfig): +class OpenIDConnectEHerkenningConfig(SingletonModel): """ Configuration for eHerkenning authentication via OpenID connect """ + oidc_rp_client_id = models.CharField( + help_text="OpenID Connect client ID provided by the OIDC Provider", + max_length=1000, + verbose_name="OpenID Connect client ID", + ) + oidc_rp_client_secret = models.CharField( + help_text="OpenID Connect secret provided by the OIDC Provider", + max_length=1000, + verbose_name="OpenID Connect secret", + ) + oidc_rp_sign_algo = models.CharField( + default="HS256", + help_text="Algorithm the Identity Provider uses to sign ID tokens", + max_length=50, + verbose_name="OpenID sign algorithm", + ) + oidc_op_discovery_endpoint = models.URLField( + blank=True, + help_text="URL of your OpenID Connect provider discovery endpoint ending with a slash (`.well-known/...` will be added automatically). If this is provided, the remaining endpoints can be omitted, as they will be derived from this endpoint.", + max_length=1000, + verbose_name="Discovery endpoint", + ) + oidc_op_jwks_endpoint = models.URLField( + blank=True, + help_text="URL of your OpenID Connect provider JSON Web Key Set endpoint. Required if `RS256` is used as signing algorithm.", + max_length=1000, + verbose_name="JSON Web Key Set endpoint", + ) + oidc_op_authorization_endpoint = models.URLField( + help_text="URL of your OpenID Connect provider authorization endpoint", + max_length=1000, + verbose_name="Authorization endpoint", + ) + oidc_op_token_endpoint = models.URLField( + help_text="URL of your OpenID Connect provider token endpoint", + max_length=1000, + verbose_name="Token endpoint", + ) + oidc_op_user_endpoint = models.URLField( + help_text="URL of your OpenID Connect provider userinfo endpoint", + max_length=1000, + verbose_name="User endpoint", + ) + oidc_rp_idp_sign_key = models.CharField( + blank=True, + help_text="Key the Identity Provider uses to sign ID tokens in the case of an RSA sign algorithm. Should be the signing key in PEM or DER format.", + max_length=1000, + verbose_name="Sign key", + ) + oidc_use_nonce = models.BooleanField( + default=True, + help_text="Controls whether the OpenID Connect client uses nonce verification", + verbose_name="Use nonce", + ) + oidc_nonce_size = models.PositiveIntegerField( + default=32, + help_text="Sets the length of the random string used for OpenID Connect nonce verification", + verbose_name="Nonce size", + ) + oidc_state_size = models.PositiveIntegerField( + default=32, + help_text="Sets the length of the random string used for OpenID Connect state verification", + verbose_name="State size", + ) + oidc_exempt_urls = ArrayField( + base_field=models.CharField(max_length=1000, verbose_name="Exempt URL"), + blank=True, + default=list, + help_text="This is a list of absolute url paths, regular expressions for url paths, or Django view names. This plus the mozilla-django-oidc urls are exempted from the session renewal by the SessionRefresh middleware.", + size=None, + verbose_name="URLs exempt from session renewal", + ) + userinfo_claims_source = models.CharField( + choices=[ + ("userinfo_endpoint", "Userinfo endpoint"), + ("id_token", "ID token"), + ], + default="userinfo_endpoint", + help_text="Indicates the source from which the user information claims should be extracted.", + max_length=100, + verbose_name="user information claims extracted from", + ) + oidc_op_logout_endpoint = models.URLField( + blank=True, + help_text="URL of your OpenID Connect provider logout endpoint", + max_length=1000, + verbose_name="Logout endpoint", + ) + oidc_keycloak_idp_hint = models.CharField( + blank=True, + help_text="Specific for Keycloak: parameter that indicates which identity provider should be used (therefore skipping the Keycloak login screen).", + max_length=1000, + verbose_name="Keycloak Identity Provider hint", + ) enabled = models.BooleanField( - _("enable"), default=False, - help_text=_( - "Indicates whether OpenID Connect for authentication/authorization is enabled. " - "This overrides overrides the usage of SAML for eHerkenning authentication." - ), + help_text="Indicates whether OpenID Connect for authentication/authorization is enabled. This overrides overrides the usage of SAML for eHerkenning authentication.", + verbose_name="enable", ) - identifier_claim_name = models.CharField( - _("KVK claim name"), - max_length=100, - help_text=_("The name of the claim in which the KVK of the user is stored"), default="kvk", + help_text="The name of the claim in which the KVK of the user is stored", + max_length=100, + verbose_name="KVK claim name", ) oidc_rp_scopes_list = ArrayField( - verbose_name=_("OpenID Connect scopes"), - base_field=models.CharField(_("OpenID Connect scope"), max_length=50), + base_field=models.CharField(max_length=50, verbose_name="OpenID Connect scope"), + blank=True, default=get_default_scopes_kvk, + help_text="OpenID Connect scopes that are requested during login. These scopes are hardcoded and must be supported by the identity provider", + size=None, + verbose_name="OpenID Connect scopes", + ) + + # TODO: this is not in django-digid-eherkenning + error_message_mapping = models.JSONField( blank=True, - help_text=_( - "OpenID Connect scopes that are requested during login. " - "These scopes are hardcoded and must be supported by the identity provider" - ), + default=dict, + help_text="Mapping that maps error messages returned by the identity provider to human readable error messages that are shown to the user", + max_length=1000, + verbose_name="Error message mapping", ) @classproperty