-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
⚗️ Most of refactor prototyping done
Probably best now to move the backends/library code into mozilla-django-oidc-db and continue the refactor after that.
- Loading branch information
1 parent
27e4c69
commit 4d839da
Showing
5 changed files
with
292 additions
and
21 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
""" | ||
Custom OIDC Authentication backend(s) which looks up configuration from the DB. | ||
TODO: contribute this back to the mozilla_django_oidc_db package. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import TypeAlias, cast | ||
|
||
from django.contrib.auth import get_user_model | ||
from django.contrib.auth.models import AbstractBaseUser, AnonymousUser | ||
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation | ||
from django.http import HttpRequest | ||
|
||
from mozilla_django_oidc.auth import OIDCAuthenticationBackend as BaseBackend | ||
from mozilla_django_oidc_db.models import OpenIDConnectConfigBase | ||
from typing_extensions import override | ||
|
||
from .models import OpenIDConnectBaseConfig | ||
from .utils import dynamic_setting, get_setting_from_config, lookup_config | ||
|
||
AnyUser: TypeAlias = AnonymousUser | AbstractBaseUser | ||
|
||
|
||
class OIDCAuthBackend(BaseBackend): | ||
""" | ||
Custom backend modifying the upstream package behaviour. | ||
This backend is meant to look up the configuration (class) to use via the config | ||
query parameter (pointing to a model class now, probably a unique key in the | ||
future), which loads the specific configuration parameters for the identity | ||
provider. | ||
No configuration is loaded in :meth:`__init__` at all, instead we define properties | ||
to dynamically look this up. Django instantiates backends *a lot*, e.g. during | ||
permission checks. We only support the :meth:`authenticate` entrypoint like the | ||
upstream library. | ||
.. todo:: go over the mozilla-django-oidc-db OIDCAuthenticationBackend methods and | ||
see what else needs to be included. | ||
""" | ||
|
||
config_class: type[OpenIDConnectConfigBase] | ||
|
||
# These should be functionaly equivalent to | ||
# :class:`mozilla_django_oidc.auth.OIDCAuthenticationBackend`. | ||
OIDC_OP_TOKEN_ENDPOINT = dynamic_setting[str]() | ||
OIDC_OP_USER_ENDPOINT = dynamic_setting[str]() | ||
OIDC_OP_JWKS_ENDPOINT = dynamic_setting[str | None](default=None) | ||
OIDC_RP_CLIENT_ID = dynamic_setting[str]() | ||
OIDC_RP_CLIENT_SECRET = dynamic_setting[str]() | ||
OIDC_RP_SIGN_ALGO = dynamic_setting[str](default="HS256") | ||
OIDC_RP_IDP_SIGN_KEY = dynamic_setting[str | None](default=None) | ||
|
||
@override | ||
def __init__(self, *args, **kwargs) -> None: | ||
# Deliberately empty, we discard all initialization from the parent class which | ||
# requires a config_class to be set. Even if we set it (declaratively), this is | ||
# not viable because it performs DB/cache IO to look up the config instance, | ||
# which would happen as well when Django goes through the auth backends for | ||
# permission checks. | ||
# | ||
# See https://github.com/maykinmedia/mozilla-django-oidc-db/issues/30 | ||
self.UserModel = get_user_model() | ||
|
||
@override | ||
def get_settings(self, attr: str, *args): # type: ignore | ||
""" | ||
Override the upstream library get_settings. | ||
Upstream is django-settings based, and we store configuration in database | ||
records instead. We look up the configuration from the DB and check if the | ||
requested setting is defined there or not. If not, it is taken from the Django | ||
settings. | ||
""" | ||
assert hasattr(self, "config_class"), ( | ||
"The config must be loaded from the authenticate entrypoint. It looks like " | ||
"you're trying to access configuration before this was called." | ||
) | ||
if (config := getattr(self, "_config", None)) is None: | ||
# django-solo and type checking is challenging, but a new release is on the | ||
# way and should fix that :fingers_crossed: | ||
config = cast(OpenIDConnectConfigBase, self.config_class.get_solo()) | ||
self._config = config | ||
self._validate_settings() | ||
return get_setting_from_config(config, attr, *args) | ||
|
||
def _validate_settings(self): | ||
if ( | ||
self.OIDC_RP_SIGN_ALGO.startswith("RS") | ||
or self.OIDC_RP_SIGN_ALGO.startswith("ES") | ||
) and ( | ||
self.OIDC_RP_IDP_SIGN_KEY is None and self.OIDC_OP_JWKS_ENDPOINT is None | ||
): | ||
msg = "{} alg requires OIDC_RP_IDP_SIGN_KEY or OIDC_OP_JWKS_ENDPOINT to be configured." | ||
raise ImproperlyConfigured(msg.format(self.OIDC_RP_SIGN_ALGO)) | ||
|
||
def _check_candidate_backend(self) -> bool: | ||
return self.get_settings("enabled") | ||
|
||
# The method signature is checked by django when selecting a suitable backend. Our | ||
# signature is more strict than the upstream library. Check the upstream | ||
# `OIDCAuthenticationCallbackView` for the `auth.authenticate(**kwargs)` call if this | ||
# needs updating. | ||
@override | ||
def authenticate( # type: ignore | ||
self, | ||
request: HttpRequest | None, | ||
nonce: str | None = None, | ||
code_verifier: str | None = None, | ||
) -> AnyUser | None: | ||
""" | ||
Authenticate the user with OIDC *iff* the conditions are met. | ||
Return ``None`` to skip to the next backend, raise | ||
:class:`django.core.exceptions.PermissionDenied` to stop in our tracks. Return | ||
a user object (real or anonymous) to signify success. | ||
""" | ||
# if we don't get a request, we can't check anything, so skip to the next | ||
# backend. We need to grab the state and code from request.GET for OIDC. | ||
if request is None: | ||
return None | ||
|
||
# Load the config to apply and check if this backend should be considered to | ||
# authenticate the user. | ||
self.config_class = lookup_config(request) | ||
is_candidate = self._check_candidate_backend() | ||
if not is_candidate: | ||
return None | ||
|
||
# Allright, now try to actually authenticate the user. | ||
return super().authenticate(request, nonce=nonce, code_verifier=code_verifier) | ||
|
||
|
||
class DigiDEHerkenningOIDCBackend(OIDCAuthBackend): | ||
""" | ||
A backend specialised to the digid-eherkenning-generics subclassed model. | ||
""" | ||
|
||
@override | ||
def _check_candidate_backend(self) -> bool: | ||
# if we're dealing with a mozilla-django-oidc-db config that is *not* a | ||
# digid-eherkenning-generics subclass, then don't bother. | ||
if not issubclass(self.config_class, OpenIDConnectBaseConfig): | ||
return False | ||
return super()._check_candidate_backend() | ||
|
||
def get_or_create_user(self, access_token: str, id_token: str, payload: dict): | ||
# TODO: overrides here - make it work for both real and "fake" users. | ||
assert isinstance(self.request, HttpRequest) | ||
|
||
user_info = self.get_userinfo(access_token, id_token, payload) | ||
|
||
claims_verified = self.verify_claims(user_info) | ||
if not claims_verified: | ||
msg = "Claims verification failed" | ||
raise SuspiciousOperation(msg) | ||
|
||
# breakpoint() | ||
|
||
user = AnonymousUser() | ||
user.is_active = True # type: ignore | ||
return user |
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
Oops, something went wrong.