Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

mozilla-django-oidc-db

Package Overview
Dependencies
Maintainers
2
Versions
39
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mozilla-django-oidc-db - npm Package Compare versions

Comparing version
1.1.1
to
0.25.2
+2
-82
CHANGELOG.rst

@@ -5,4 +5,4 @@ =========

1.1.1 (2025-11-19)
==================
0.25.2 (2025-11-19)
===================

@@ -14,82 +14,2 @@ Hotfix release.

1.1.0 (2025-11-18)
==================
Minor release.
* [#157] Relocate shared test utility ``keycloak_login`` to ``mozilla_django_oidc_db/tests/utils``
to make sure upstream projects can make use of it
1.0.2 (2025-10-24)
==================
Bugfix release - same patch as 1.0.1 but fixed some missed cases.
1.0.1 (2025-10-24)
==================
Bugfix release.
* Relaxed the user model inheritance check in the backend.
1.0.0 (2025-10-23)
==================
After a long time we feel the library is finally ready for a 1.0 version!
Releases 0.17.0 and 0.24.0 included a large rework of the architecture of the library,
which we considered essential before even thinking of a 1.0 version. Since then, we've
found no major issues and have adapted the library in a number of real projects in
production with varying degrees of complexity.
From now on, breaking changes will result in a major version bump.
This release itself contains some (technically) breaking changes, but we expect they won't
really affect you.
**💥 Breaking changes**
* Dropped support for Python 3.10
* Dropped support for Python 3.11
* Reworked types and classes used for the plugin system, in particular:
* Removed :class:`mozilla_django_oidc_db.plugins.OIDCBasePluginProtocol`, instead there is
an abstract base class :class:`mozilla_django_oidc_db.plugins.BaseOIDCPlugin`.
* Removed :class:`mozilla_django_oidc_db.plugins.BaseOIDCPlugin`, instead there is
:class:`mozilla_django_oidc_db.plugins.BaseOIDCPlugin`.
* Removed :class:`mozilla_django_oidc_db.plugins.AnonymousUserOIDCPluginProtocol`,
instead there is an abstract base class
:class:`mozilla_django_oidc_db.plugins.AnonymousUserOIDCPlugin`.
* Removed :class:`mozilla_django_oidc_db.plugins.AbstractUserOIDCPluginProtocol`,
instead there is an abstract base class
:class:`mozilla_django_oidc_db.plugins.AbstractUserOIDCPlugin`.
Typically now you should only be subclassing either ``AnonymousUserOIDCPlugin`` or
``AbstractUserOIDCPlugin`` - they inherit from the abstract base class and provide
all necessary functionalities.
* The django-setup-configuration format appears to not be (fully) backwards compatible
since release 0.24.0. Downstream projects should mention this in their changelogs
and/or provide a migration path.
**New features**
* [`#121`_] Added Dutch translations.
**Bugfixes**
* [`#120`_] Fixed the retrieval of optional endpoints causing database errors.
* [`#113`_] Removed Open Forms reference in generic failure template.
**Project maintenance**
* [`#154`_] Improved documentation for setup-configuration integration.
* Improved the static type hints and added type-checking to the CI pipeline.
* Updated to modern Python syntax.
.. _#154: https://github.com/maykinmedia/mozilla-django-oidc-db/issues/154
.. _#120: https://github.com/maykinmedia/mozilla-django-oidc-db/issues/120
.. _#113: https://github.com/maykinmedia/mozilla-django-oidc-db/issues/113
.. _#121: https://github.com/maykinmedia/mozilla-django-oidc-db/issues/121
0.25.1 (2025-08-25)

@@ -96,0 +16,0 @@ ===================

+8
-8
Metadata-Version: 2.4
Name: mozilla-django-oidc-db
Version: 1.1.1
Version: 0.25.2
Summary: A database-backed configuration for mozilla-django-oidc

@@ -13,3 +13,3 @@ Author-email: Maykin Media <support@maykinmedia.nl>

Keywords: OIDC,django,database,authentication
Classifier: Development Status :: 5 - Production/Stable
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Django

@@ -22,6 +22,8 @@ Classifier: Framework :: Django :: 4.2

Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Requires-Python: >=3.10
Description-Content-Type: text/x-rst

@@ -35,3 +37,3 @@ License-File: LICENSE

Provides-Extra: setup-configuration
Requires-Dist: django-setup-configuration>=0.11.0; extra == "setup-configuration"
Requires-Dist: django-setup-configuration>=0.8.2; extra == "setup-configuration"
Provides-Extra: tests

@@ -49,4 +51,2 @@ Requires-Dist: psycopg; extra == "tests"

Requires-Dist: ruff; extra == "tests"
Requires-Dist: pyright; extra == "tests"
Requires-Dist: django-stubs; extra == "tests"
Provides-Extra: docs

@@ -68,3 +68,3 @@ Requires-Dist: sphinx; extra == "docs"

:Version: 1.1.1
:Version: 0.25.2
:Source: https://github.com/maykinmedia/mozilla-django-oidc-db

@@ -108,3 +108,3 @@ :Keywords: OIDC, django, database, authentication

.. |coverage| image:: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db/branch/master/graph/badge.svg
:target: https://app.codecov.io/gh/maykinmedia/mozilla-django-oidc-db
:target: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db
:alt: Coverage status

@@ -111,0 +111,0 @@

@@ -15,3 +15,3 @@ django>=4.2

[setup-configuration]
django-setup-configuration>=0.11.0
django-setup-configuration>=0.8.2

@@ -30,3 +30,1 @@ [tests]

ruff
pyright
django-stubs

@@ -32,3 +32,2 @@ CHANGELOG.rst

mozilla_django_oidc_db.egg-info/top_level.txt
mozilla_django_oidc_db/locale/nl/LC_MESSAGES/django.po
mozilla_django_oidc_db/migrations/0001_initial_to_v015.py

@@ -53,3 +52,2 @@ mozilla_django_oidc_db/migrations/0001_initial_to_v023.py

mozilla_django_oidc_db/tests/mixins.py
mozilla_django_oidc_db/tests/utils.py
tests/test_admin_form.py

@@ -56,0 +54,0 @@ tests/test_backend.py

from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, override
from typing import Any, cast

@@ -17,2 +17,3 @@ from django.contrib.auth import get_user_model

from mozilla_django_oidc.auth import OIDCAuthenticationBackend as BaseBackend
from typing_extensions import override

@@ -23,3 +24,6 @@ from .config import dynamic_setting, lookup_config

from .models import OIDCClient, UserInformationClaimsSources
from .plugins import AbstractUserOIDCPlugin, AnonymousUserOIDCPlugin
from .plugins import (
AbstractUserOIDCPluginProtocol,
AnonymousUserOIDCPluginProtocol,
)
from .registry import register as registry

@@ -77,9 +81,6 @@ from .typing import JSONObject

# AbstractUser.
UserModel = get_user_model()
if TYPE_CHECKING:
assert issubclass(UserModel, AbstractUser)
self.UserModel = UserModel
self.UserModel = cast(AbstractUser, get_user_model())
@override
def get_settings(self, attr: str, *args: Any) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
def get_settings(self, attr: str, *args: Any) -> Any: # type: ignore
"""

@@ -112,3 +113,3 @@ Override the upstream library get_settings.

@override
def authenticate( # pyright: ignore[reportIncompatibleMethodOverride]
def authenticate( # type: ignore
self,

@@ -145,3 +146,3 @@ request: HttpRequest | None,

if user:
user._oidcdb_config_identifier = self.config.identifier # pyright: ignore[reportAttributeAccessIssue]
user._oidcdb_config_identifier = self.config.identifier # type: ignore

@@ -157,3 +158,3 @@ return user

plugin = registry[self.config.identifier]
assert isinstance(plugin, AbstractUserOIDCPlugin)
assert isinstance(plugin, AbstractUserOIDCPluginProtocol)
return plugin.verify_claims(claims)

@@ -223,7 +224,7 @@

self, claims: JSONObject
) -> models.QuerySet[AbstractUser]:
) -> models.Manager[AbstractUser]: # type: ignore (parent function returns UserManager which is more specific than Manager)
assert self.config
plugin = registry[self.config.identifier]
assert isinstance(plugin, AbstractUserOIDCPlugin)
assert isinstance(plugin, AbstractUserOIDCPluginProtocol)
return plugin.filter_users_by_claims(claims)

@@ -237,3 +238,3 @@

assert isinstance(plugin, AbstractUserOIDCPlugin)
assert isinstance(plugin, AbstractUserOIDCPluginProtocol)
return plugin.create_user(claims)

@@ -246,3 +247,3 @@

assert isinstance(plugin, AbstractUserOIDCPlugin)
assert isinstance(plugin, AbstractUserOIDCPluginProtocol)
return plugin.update_user(user, claims)

@@ -253,3 +254,3 @@

self, access_token: str, id_token: str, payload: JSONObject
) -> AnonymousUser | AbstractUser | None:
) -> AnonymousUser | AbstractUser | None: # type: ignore (parent function returns only an AbstractUser | None)
"""Get or create a user based on the tokens received."""

@@ -261,5 +262,3 @@ assert self.config

# shortcut for "anonymous users" where the OIDC authentication *does* happen,
# but no actual Django user instance is created.
if isinstance(plugin, AnonymousUserOIDCPlugin):
if isinstance(plugin, AnonymousUserOIDCPluginProtocol):
return plugin.get_or_create_user(

@@ -269,5 +268,2 @@ access_token, id_token, payload, self.request

user = super().get_or_create_user(access_token, id_token, payload)
if user is not None:
assert isinstance(user, self.UserModel)
return user
return super().get_or_create_user(access_token, id_token, payload)

@@ -9,3 +9,3 @@ """

from typing import Any, Protocol, Self, TypeVar, Unpack, overload
from typing import Any, Generic, Protocol, TypeVar, overload

@@ -18,3 +18,3 @@ from django.core.exceptions import (

from mozilla_django_oidc.utils import import_from_settings
from typing_extensions import TypedDict
from typing_extensions import Self, TypedDict, Unpack

@@ -75,7 +75,7 @@ from .constants import CONFIG_IDENTIFIER_SESSION_KEY

class DynamicSettingKwargs[T](TypedDict, total=False):
class DynamicSettingKwargs(TypedDict, Generic[T], total=False):
default: T
class dynamic_setting[T]:
class dynamic_setting(Generic[T]):
"""

@@ -82,0 +82,0 @@ Descriptor to lazily access settings while explicitly defining them.

@@ -1,8 +0,4 @@

from collections.abc import Mapping
from .typing import EndpointFieldNames
# Mapping the configuration model fieldnames for endpoints to their
# corresponding names in the OIDC spec
OIDC_MAPPING: Mapping[EndpointFieldNames, str] = {
OIDC_MAPPING = {
"oidc_op_authorization_endpoint": "authorization_endpoint",

@@ -9,0 +5,0 @@ "oidc_op_token_endpoint": "token_endpoint",

import json
from collections.abc import Mapping
from urllib.parse import urljoin

@@ -12,7 +10,4 @@ from django import forms

from .models import OIDCProvider
from .typing import EndpointFieldNames
type EndpointsMapping = Mapping[EndpointFieldNames, str]
class OIDCProviderForm(forms.ModelForm):

@@ -53,11 +48,10 @@ required_endpoints = [

@classmethod
def get_endpoints_from_discovery(cls, base_url: str) -> EndpointsMapping:
response = requests.get(urljoin(base_url, OPEN_ID_CONFIG_PATH), timeout=10)
def get_endpoints_from_discovery(cls, base_url: str):
response = requests.get(f"{base_url}{OPEN_ID_CONFIG_PATH}", timeout=10)
response.raise_for_status()
configuration = response.json()
endpoints: EndpointsMapping = {
model_attr: endpoint
endpoints = {
model_attr: configuration.get(oidc_attr)
for model_attr, oidc_attr in cls.oidc_mapping.items()
if (endpoint := configuration.get(oidc_attr))
}

@@ -67,5 +61,5 @@ return endpoints

def clean(self):
super().clean()
cleaned_data = super().clean()
discovery_endpoint = self.cleaned_data.get("oidc_op_discovery_endpoint")
discovery_endpoint = cleaned_data.get("oidc_op_discovery_endpoint")

@@ -76,3 +70,3 @@ # Derive the endpoints from the discovery endpoint

endpoints = self.get_endpoints_from_discovery(discovery_endpoint)
self.cleaned_data.update(**endpoints)
cleaned_data.update(**endpoints)
except (

@@ -93,5 +87,5 @@ requests.exceptions.RequestException,

for field in self.required_endpoints:
if not self.cleaned_data.get(field):
if not cleaned_data.get(field):
self.add_error(field, _("This field is required."))
return self.cleaned_data
return cleaned_data

@@ -1,2 +0,2 @@

from typing import Any, override
from typing import Any

@@ -6,2 +6,3 @@ from django.urls import reverse

from mozilla_django_oidc.middleware import SessionRefresh as BaseSessionRefresh
from typing_extensions import override

@@ -72,7 +73,4 @@ from .config import (

# we can't use cached_property, because a middleware instance exists for the whole
# duration of the django server life cycle, and the relevant config can change
# between requests. See ``process_request``.
@property
def exempt_urls(self): # pyright: ignore[reportIncompatibleVariableOverride]
def exempt_urls(self):
# In many cases, the OIDC_AUTHENTICATION_CALLBACK_URL will be the generic

@@ -79,0 +77,0 @@ # callback handler and already be part of super().exempt_urls. However, this is

@@ -63,3 +63,3 @@ # Generated by Django 4.2.9 on 2024-05-01 15:32

"oidc_rp_scopes_list",
django_jsonform.models.fields.ArrayField( # type: ignore
django_jsonform.models.fields.ArrayField(
base_field=models.CharField(

@@ -180,3 +180,3 @@ max_length=50, verbose_name="OpenID Connect scope"

"oidc_exempt_urls",
django_jsonform.models.fields.ArrayField( # type: ignore
django_jsonform.models.fields.ArrayField(
base_field=models.CharField(

@@ -240,3 +240,3 @@ max_length=1000, verbose_name="Exempt URL"

"superuser_group_names",
django_jsonform.models.fields.ArrayField( # type: ignore
django_jsonform.models.fields.ArrayField(
base_field=models.CharField(

@@ -243,0 +243,0 @@ max_length=50, verbose_name="Superuser group name"

@@ -75,3 +75,3 @@ # Generated by Django 5.0.4 on 2025-08-01 09:25

"oidc_rp_scopes_list",
django_jsonform.models.fields.ArrayField( # type: ignore
django_jsonform.models.fields.ArrayField(
base_field=models.CharField(

@@ -219,3 +219,3 @@ max_length=50, verbose_name="OpenID Connect scope"

"superuser_group_names",
django_jsonform.models.fields.ArrayField( # type: ignore
django_jsonform.models.fields.ArrayField(
base_field=models.CharField(

@@ -222,0 +222,0 @@ max_length=50, verbose_name="Superuser group name"

@@ -17,3 +17,3 @@ # Generated by Django 4.2.9 on 2024-05-01 16:10

name="new_groups_claim",
field=mozilla_django_oidc_db.fields.ClaimField( # type: ignore
field=mozilla_django_oidc_db.fields.ClaimField(
base_field=models.CharField(

@@ -32,3 +32,3 @@ max_length=50, verbose_name="claim path segment"

name="new_username_claim",
field=mozilla_django_oidc_db.fields.ClaimField( # type: ignore
field=mozilla_django_oidc_db.fields.ClaimField(
base_field=models.CharField(

@@ -35,0 +35,0 @@ max_length=50, verbose_name="claim path segment"

@@ -178,3 +178,3 @@ # Generated by Django 4.2.20 on 2025-05-08 10:06

"oidc_rp_scopes_list",
django_jsonform.models.fields.ArrayField( # type: ignore
django_jsonform.models.fields.ArrayField(
base_field=models.CharField(

@@ -181,0 +181,0 @@ max_length=50, verbose_name="Scope"

from __future__ import annotations
import logging
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
from abc import abstractmethod
from typing import Any, Protocol, TypeAlias, runtime_checkable

@@ -11,3 +11,3 @@ from django.contrib.auth import get_user_model

from django.db import models
from django.http import HttpRequest, HttpResponseBase
from django.http import HttpRequest, HttpResponse

@@ -22,3 +22,3 @@ from glom import Path, glom

from .schemas import ADMIN_OPTIONS_SCHEMA
from .typing import ClaimPath, GetParams, JSONObject
from .typing import ClaimPath, JSONObject
from .utils import get_groups_by_name, obfuscate_claims

@@ -31,52 +31,14 @@ from .views import AdminCallbackView

#
# ABSTRACT BASE CLASSES
#
class BaseOIDCPlugin(ABC):
"""
Base class/interface for all plugins to implement.
"""
class OIDCBasePluginProtocol(Protocol):
identifier: str
"""
The unique identifier for the plugin.
schema: JSONObject
Typically provided through the ``@register(IDENTIFIER)`` decorator when registering
a plugin in downstream code.
"""
def get_config(self) -> OIDCClient: ...
def __init__(self, identifier: str):
self.identifier = identifier
def get_setting(self, attr: str, *args) -> Any: ...
def get_config(self) -> OIDCClient:
"""
Resolve the instance holding the configuration options.
"""
return OIDCClient.objects.resolve(self.identifier)
def get_setting(self, attr: str, *args) -> Any:
"""
Look up a particular configuration parameter for the configuration options.
:param attr: The setting/configuration parameter to look up.
:param args: Any additional arguments for the lookup behaviour, typically a
default value for missing settings is provided here.
"""
config = self.get_config()
return get_setting_from_config(config, attr, *args)
@abstractmethod
def get_schema(self) -> JSONObject:
"""
Return the JSON Schema definition for the client configuration options.
Each plugin provides certain behaviour that may have configuration parameters.
The configuration parameters are stored in the ``options`` JSONField of the
:class:`~mozilla_django_oidc_db.models.OIDCClient` model.
The admin integration needs a JSON Schema definitions to be able to configure
and validate the options when editing the client configuration.
"""
"""Get the JSON schema of the ``options`` field on the :class:`~mozilla_django_oidc_db.models.OIDCClient` model."""
...

@@ -86,95 +48,72 @@

def validate_settings(self) -> None:
"""
Check the validity of the settings in the provider and client configuration.
:raises ImproperlyConfigured: if invalid configuration is detected.
"""
"""Check the validity of the settings in the provider and client configuration."""
...
def get_extra_params(
self, request: HttpRequest, extra_params: GetParams
) -> GetParams:
"""
Return (additional) ``GET`` parameters for the redirect to the identity provider.
@abstractmethod
def handle_callback(self, request: HttpRequest) -> HttpResponse:
"""Return an HttpResponse using a specific callback view.
By default, the passed in ``extra_params`` are returned unmodified.
.. code:: python
:arg extra_params: A mapping of query parameters already produced by
:class:`mozilla_django_oidc_db.views.OIDCAuthenticationRequestInitView`.
def handle_callback(self, request: HttpRequest) -> HttpResponse:
return admin_callback_view(request)
"""
return extra_params
...
@abstractmethod
def handle_callback(self, request: HttpRequest) -> HttpResponseBase:
"""
Return an HttpResponse using a specific callback view.
def get_extra_params(
self, request: HttpRequest, extra_params: dict[str, str | bytes]
) -> dict[str, str | bytes]: ...
Typed as ``HttpResponseBase`` because that's the annotation for
``View.as_view()`` in django-stubs.
For example:
class BaseOIDCPlugin:
def __init__(self, identifier: str):
self.identifier = identifier
.. code:: python
def get_config(self) -> OIDCClient:
return OIDCClient.objects.resolve(self.identifier)
def handle_callback(self, request: HttpRequest) -> HttpResponseBase:
return admin_callback_view(request)
def get_setting(self, attr: str, *args) -> Any:
config = self.get_config()
"""
...
return get_setting_from_config(config, attr, *args)
def get_extra_params(
self, request: HttpRequest, extra_params: dict[str, str | bytes]
) -> dict[str, str | bytes]:
return extra_params
class AnonymousUserOIDCPlugin(BaseOIDCPlugin):
if TYPE_CHECKING:
def get_or_create_user(
self,
access_token: str,
id_token: str,
payload: JSONObject,
request: HttpRequest,
) -> AnonymousUser | None: ...
@runtime_checkable
class AnonymousUserOIDCPluginProtocol(OIDCBasePluginProtocol, Protocol):
def get_or_create_user(
self,
access_token: str,
id_token: str,
payload: JSONObject,
request: HttpRequest,
) -> AnonymousUser | None: ...
class AbstractUserOIDCPlugin(BaseOIDCPlugin):
if TYPE_CHECKING:
@runtime_checkable
class AbstractUserOIDCPluginProtocol(OIDCBasePluginProtocol, Protocol):
def create_user(self, claims: JSONObject) -> AbstractUser: ...
@abstractmethod
def create_user(self, claims: JSONObject) -> AbstractUser:
"""
Create and return the Django user in the database from the validated claims.
"""
...
def update_user(self, user: AbstractUser, claims: JSONObject) -> AbstractUser: ...
@abstractmethod
def update_user(self, user: AbstractUser, claims: JSONObject) -> AbstractUser:
"""
Update and return the Django user in the database from the validated claims.
"""
...
def filter_users_by_claims(
self, claims: JSONObject
) -> models.Manager[AbstractUser]:
"""Return all users matching the specified subject."""
...
@abstractmethod
def filter_users_by_claims(
self,
claims: JSONObject,
) -> models.QuerySet[AbstractUser]:
"""
Given the validated claims, filter for existing users in the database.
def verify_claims(self, claims: JSONObject) -> bool:
"""Verify the provided claims to decide if authentication should be allowed."""
...
This method is called to test if a user already exists that should be
updated rather than created.
"""
...
def verify_claims(self, claims: JSONObject) -> bool:
"""
Verify the provided claims to decide if authentication should be allowed.
"""
...
OIDCPlugin: TypeAlias = AbstractUserOIDCPluginProtocol | AnonymousUserOIDCPluginProtocol
#
# CONCRETE IMPLEMENTATIONS
#
admin_callback_view = AdminCallbackView.as_view()

@@ -184,6 +123,4 @@

@register(OIDC_ADMIN_CONFIG_IDENTIFIER)
class OIDCAdminPlugin(AbstractUserOIDCPlugin):
"""
Implement the core plugin for admin authentication via OpenID Connect.
"""
class OIDCAdminPlugin(BaseOIDCPlugin, AbstractUserOIDCPluginProtocol):
schema = ADMIN_OPTIONS_SCHEMA

@@ -248,9 +185,5 @@ def verify_claims(self, claims: JSONObject) -> bool:

self, claims: JSONObject
) -> models.QuerySet[AbstractUser]:
) -> models.Manager[AbstractUser]:
"""Return all users matching the specified subject."""
UserModel = get_user_model()
if TYPE_CHECKING:
assert issubclass(UserModel, AbstractUser), (
"The user model must inherit from AbstractUser."
)

@@ -273,8 +206,2 @@ username = self.get_username(claims)

"""Return object for a newly created user account."""
UserModel = get_user_model()
if TYPE_CHECKING:
assert issubclass(UserModel, AbstractUser), (
"The user model must inherit from AbstractUser."
)
username = self.get_username(claims)

@@ -284,2 +211,3 @@

UserModel = get_user_model()
user = UserModel.objects.create_user(**{UserModel.USERNAME_FIELD: username})

@@ -420,3 +348,3 @@ self.update_user(user, claims)

def handle_callback(self, request: HttpRequest) -> HttpResponseBase:
def handle_callback(self, request: HttpRequest) -> HttpResponse:
return admin_callback_view(request)
from __future__ import annotations
from collections.abc import Callable, Iterable, MutableMapping
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING

@@ -9,7 +9,7 @@

if TYPE_CHECKING:
from .plugins import BaseOIDCPlugin
from .plugins import OIDCPlugin
class OIDCRegistry[T: type[BaseOIDCPlugin]]:
_registry: MutableMapping[str, T]
class OIDCRegistry:
_registry: dict[str, OIDCPlugin]

@@ -19,3 +19,5 @@ def __init__(self):

def __call__(self, unique_identifier: str) -> Callable[[T], T]:
def __call__(
self, unique_identifier: str
) -> Callable[[type[OIDCPlugin]], type[OIDCPlugin]]:
if len(unique_identifier) > UNIQUE_PLUGIN_ID_MAX_LENGTH:

@@ -27,3 +29,3 @@ raise ValueError(

def decorator(plugin_cls: T) -> T:
def decorator(plugin_cls: type[OIDCPlugin]) -> type[OIDCPlugin]:
if unique_identifier in self._registry:

@@ -41,3 +43,3 @@ raise ValueError(

def items(self) -> Iterable[tuple[str, T]]:
def items(self) -> Iterable[tuple[str, OIDCPlugin]]:
return self._registry.items()

@@ -48,3 +50,3 @@

def __getitem__(self, key: str) -> T:
def __getitem__(self, key: str) -> OIDCPlugin:
return self._registry[key]

@@ -51,0 +53,0 @@

@@ -73,3 +73,3 @@ from typing import Annotated, Literal

description="a unique identifier for this OIDC provider.",
examples=["test-oidc-provider"],
examples=["admin-oidc-provider"],
)

@@ -99,27 +99,3 @@ endpoint_config: OIDCProviderConfigUnion

oidc_rp_scopes_list: list[str] = DjangoModelRef(OIDCClient, "oidc_rp_scopes_list")
options: dict = DjangoModelRef(
OIDCClient,
"options",
default_factory=dict,
examples=[
{
"user_settings": {
"claim_mappings": {
"username": ["sub"],
"email": ["email"],
"first_name": ["given_name"],
"last_name": ["family_name"],
},
"username_case_sensitive": False,
},
"groups_settings": {
"make_users_staff": True,
"superuser_group_names": ["superuser"],
"sync": True,
"sync_pattern": "*",
"claim_mapping": ["roles"],
},
}
],
)
options: dict = DjangoModelRef(OIDCClient, "options", default_factory=dict)

@@ -129,3 +105,3 @@ endpoint_config: OIDCProviderConfigUnion | None = Field(

default=None,
deprecated="Moved to `providers.endpoint_config`",
deprecated=True,
)

@@ -144,3 +120,3 @@ oidc_provider_identifier: str = DjangoModelRef(

description=_("Mapping from User model field names to a path in the claim."),
deprecated="Moved to `items.options.user_settings.claim_mappings`",
deprecated=True,
)

@@ -154,3 +130,3 @@ oidc_token_use_basic_auth: bool = Field(

),
deprecated="Moved to `providers.oidc_token_use_basic_auth`",
deprecated=True,
)

@@ -160,3 +136,3 @@ oidc_use_nonce: bool = Field(

description=_("Controls whether the client uses nonce verification"),
deprecated="Moved to providers.oidc_use_nonce",
deprecated=True,
)

@@ -168,3 +144,3 @@ oidc_nonce_size: int = Field(

),
deprecated="Moved to `providers.oidc_nonce_size`",
deprecated=True,
)

@@ -176,3 +152,3 @@ oidc_state_size: int = Field(

),
deprecated="Moved to `providers.oidc_state_size`",
deprecated=True,
)

@@ -183,3 +159,3 @@ # Arrays are overridden to make the typing simpler (the underlying Django field is an ArrayField, which is non-standard)

description=_("Path in the claims to the value to use as username."),
deprecated="Moved to `items.options.user_settings.claim_mappings.username`",
deprecated=True,
examples=[["nested", "username", "claim"]],

@@ -190,3 +166,3 @@ )

description=_("Path in the claims to the value with group names."),
deprecated="Moved to `items.options.group_settings.claim_mapping`",
deprecated=True,
examples=[["nested", "group", "claim"]],

@@ -197,3 +173,3 @@ )

description=_("Superuser group names"),
deprecated="Moved to `items.options.group_settings.superuser_group_names`",
deprecated=True,
examples=[["superusers"]],

@@ -204,3 +180,3 @@ )

description=_("Default group names"),
deprecated="Moved `items.options.group_settings.default_groups`",
deprecated=True,
examples=[["read-only-users"]],

@@ -210,3 +186,3 @@ )

description=_("Whether to sync local groups"),
deprecated="Moved to `items.options.group_settings.sync`",
deprecated=True,
examples=[True],

@@ -217,3 +193,3 @@ default=True,

description=_("Pattern that the group names to sync should follow."),
deprecated="Moved to `items.options.group_settings.sync_pattern`",
deprecated=True,
examples=["*"],

@@ -224,3 +200,3 @@ default="*",

description=_("Whether to make the users staff."),
deprecated="Moved to `items.options.groups_settings.make_users_staff`",
deprecated=True,
examples=[False],

@@ -227,0 +203,0 @@ default=False,

@@ -15,3 +15,4 @@ {% extends "admin/login.html" %}

There was a problem while logging in with your organization account.
Please contact your administrator with this error message to resolve this:
Please contact your Open Forms administrator with this error message to
resolve this:
{% endblocktrans %}

@@ -27,2 +28,2 @@ </p>

{% endblock %}
{% endblock %}
from __future__ import annotations
from collections.abc import Mapping, MutableMapping, Sequence
from typing import Literal, Protocol
from collections.abc import Sequence
from typing import Protocol, TypeAlias
from django.http import HttpRequest, HttpResponseBase
type EndpointFieldNames = Literal[
"oidc_op_authorization_endpoint",
"oidc_op_token_endpoint",
"oidc_op_user_endpoint",
"oidc_op_jwks_endpoint",
"oidc_op_logout_endpoint",
]
JSONPrimitive: TypeAlias = str | int | float | bool | None
JSONValue: TypeAlias = "JSONPrimitive | list[JSONValue] | JSONObject"
JSONObject: TypeAlias = dict[str, JSONValue]
type JSONPrimitive = str | int | float | bool | None
type JSONValue = JSONPrimitive | list[JSONValue] | JSONObject
type JSONObject = MutableMapping[str, JSONValue]
ClaimPath: TypeAlias = Sequence[str]
type ClaimPath = Sequence[str]
type GetParams = Mapping[str, str | bytes]
class DjangoView(Protocol):
def __call__(self, request: HttpRequest) -> HttpResponseBase: ...

@@ -10,5 +10,3 @@ import fnmatch

from glom import Path, PathAccessError, assign, glom
from requests.utils import (
_parse_content_type_header, # pyright: ignore[reportAttributeAccessIssue]
)
from requests.utils import _parse_content_type_header

@@ -15,0 +13,0 @@ from .models import OIDCClient

@@ -9,3 +9,3 @@ import logging

from django.http import HttpRequest, HttpResponseBase, HttpResponseRedirect
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils.http import url_has_allowed_host_and_scheme

@@ -25,3 +25,2 @@ from django.views import View

from .registry import register as registry
from .typing import GetParams

@@ -107,5 +106,3 @@ logger = logging.getLogger(__name__)

@property
def failure_url(self):
return reverse("admin-oidc-error")
failure_url = reverse_lazy("admin-oidc-error")

@@ -322,15 +319,17 @@ def get(self, request: HttpRequest, *args, **kwargs):

def get_extra_params(self, request: HttpRequest) -> GetParams:
def get_extra_params(self, request: HttpRequest) -> dict[str, str]:
"""
Add a keycloak identity provider hint if configured.
"""
extra: GetParams = super().get_extra_params(request)
extra = super().get_extra_params(request)
if kc_idp_hint := self.get_settings("OIDC_KEYCLOAK_IDP_HINT", ""):
extra = {**extra, "kc_idp_hint": kc_idp_hint}
extra["kc_idp_hint"] = kc_idp_hint
plugin = registry[self.identifier]
return plugin.get_extra_params(request, extra)
extra = plugin.get_extra_params(request, extra)
return extra
class OIDCAuthenticationRequestView(OIDCAuthenticationRequestInitView):

@@ -337,0 +336,0 @@ """

Metadata-Version: 2.4
Name: mozilla-django-oidc-db
Version: 1.1.1
Version: 0.25.2
Summary: A database-backed configuration for mozilla-django-oidc

@@ -13,3 +13,3 @@ Author-email: Maykin Media <support@maykinmedia.nl>

Keywords: OIDC,django,database,authentication
Classifier: Development Status :: 5 - Production/Stable
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Django

@@ -22,6 +22,8 @@ Classifier: Framework :: Django :: 4.2

Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Requires-Python: >=3.10
Description-Content-Type: text/x-rst

@@ -35,3 +37,3 @@ License-File: LICENSE

Provides-Extra: setup-configuration
Requires-Dist: django-setup-configuration>=0.11.0; extra == "setup-configuration"
Requires-Dist: django-setup-configuration>=0.8.2; extra == "setup-configuration"
Provides-Extra: tests

@@ -49,4 +51,2 @@ Requires-Dist: psycopg; extra == "tests"

Requires-Dist: ruff; extra == "tests"
Requires-Dist: pyright; extra == "tests"
Requires-Dist: django-stubs; extra == "tests"
Provides-Extra: docs

@@ -68,3 +68,3 @@ Requires-Dist: sphinx; extra == "docs"

:Version: 1.1.1
:Version: 0.25.2
:Source: https://github.com/maykinmedia/mozilla-django-oidc-db

@@ -108,3 +108,3 @@ :Keywords: OIDC, django, database, authentication

.. |coverage| image:: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db/branch/master/graph/badge.svg
:target: https://app.codecov.io/gh/maykinmedia/mozilla-django-oidc-db
:target: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db
:alt: Coverage status

@@ -111,0 +111,0 @@

@@ -7,3 +7,3 @@ [build-system]

name = "mozilla-django-oidc-db"
version = "1.1.1"
version = "0.25.2"
description = "A database-backed configuration for mozilla-django-oidc"

@@ -18,3 +18,3 @@ authors = [

classifiers = [
"Development Status :: 5 - Production/Stable",
"Development Status :: 4 - Beta",
"Framework :: Django",

@@ -27,2 +27,4 @@ "Framework :: Django :: 4.2",

"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",

@@ -32,3 +34,3 @@ "Programming Language :: Python :: 3.13",

]
requires-python = ">=3.12"
requires-python = ">=3.10"
dependencies = [

@@ -51,3 +53,3 @@ "django>=4.2",

setup-configuration = [
"django-setup-configuration>=0.11.0",
"django-setup-configuration>=0.8.2",
]

@@ -66,4 +68,2 @@ tests = [

"ruff",
"pyright",
"django-stubs",
]

@@ -93,3 +93,3 @@ docs = [

[tool.bumpversion]
current_version = "1.1.1"
current_version = "0.25.2"
files = [

@@ -119,3 +119,3 @@ {filename = "pyproject.toml"},

"\\.\\.\\.",
"\\bpass$",
"pass",
]

@@ -126,18 +126,2 @@ omit = [

[tool.pyright]
include = [
"mozilla_django_oidc_db",
"testapp",
"tests",
]
ignore = [
# at the time of writing, the type definitions in factory-boy are still broken
"mozilla_django_oidc_db/tests/factories.py",
# don't know what's going on here, in particular because it's based on Pydantic I'd
# expect this to have zero type checking issues. TODO for later
"mozilla_django_oidc_db/setup_configuration/",
]
# pythonVersion = "3.10"
pythonPlatform = "Linux"
[tool.ruff.lint]

@@ -144,0 +128,0 @@ extend-select = [

@@ -10,3 +10,3 @@

:Version: 1.1.1
:Version: 0.25.2
:Source: https://github.com/maykinmedia/mozilla-django-oidc-db

@@ -50,3 +50,3 @@ :Keywords: OIDC, django, database, authentication

.. |coverage| image:: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db/branch/master/graph/badge.svg
:target: https://app.codecov.io/gh/maykinmedia/mozilla-django-oidc-db
:target: https://codecov.io/gh/maykinmedia/mozilla-django-oidc-db
:alt: Coverage status

@@ -53,0 +53,0 @@

@@ -133,16 +133,2 @@ from json.decoder import JSONDecodeError

@pytest.mark.vcr
def test_derive_endpoints_google_oidc():
endpoints = OIDCProviderForm.get_endpoints_from_discovery(
base_url="https://accounts.google.com"
)
assert endpoints == {
"oidc_op_authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"oidc_op_token_endpoint": "https://oauth2.googleapis.com/token",
"oidc_op_user_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
"oidc_op_jwks_endpoint": "https://www.googleapis.com/oauth2/v3/certs",
}
@pytest.mark.django_db

@@ -149,0 +135,0 @@ def test_no_discovery_endpoint_other_fields_required():

import logging
from django.contrib.auth.models import Group, User
from django.contrib.sessions.backends.base import SessionBase
from django.core.exceptions import BadRequest, ImproperlyConfigured

@@ -21,4 +20,2 @@ from django.http import HttpRequest

from .conftest import callback_request_mark as callback_request, oidcconfig
#

@@ -29,3 +26,3 @@ # DYNAMIC CONFIGURATION TESTS

@oidcconfig(enabled=False)
@pytest.mark.oidcconfig(enabled=False)
def test_authenticate_oidc_not_enabled(dummy_config, callback_request: HttpRequest):

@@ -42,4 +39,4 @@ backend = OIDCAuthenticationBackend()

@pytest.mark.django_db
@oidcconfig
@callback_request(
@pytest.mark.oidcconfig
@pytest.mark.callback_request(
init_view=OIDCAuthenticationRequestInitView.as_view(identifier="test-oidc-disabled")

@@ -98,3 +95,3 @@ )

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -144,3 +141,3 @@ extra_options={

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -177,3 +174,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -207,3 +204,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -251,3 +248,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -285,3 +282,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -317,3 +314,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -379,3 +376,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -409,3 +406,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -438,3 +435,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -473,3 +470,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -498,3 +495,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -527,3 +524,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -555,3 +552,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -590,3 +587,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(enabled=True)
@pytest.mark.oidcconfig(enabled=True)
def test_authenticate_called_without_args(dummy_config):

@@ -600,3 +597,3 @@ backend = OIDCAuthenticationBackend()

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -616,3 +613,5 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@oidcconfig(enabled=True, userinfo_claims_source=UserInformationClaimsSources.id_token)
@pytest.mark.oidcconfig(
enabled=True, userinfo_claims_source=UserInformationClaimsSources.id_token
)
def test_empty_claims(dummy_config, callback_request: HttpRequest):

@@ -626,3 +625,3 @@ backend = MockBackend(claims={})

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -659,3 +658,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

request = rf.get("/oidc/callback", {"state": "foo", "code": "bar"})
request.session = SessionBase()
request.session = {}
backend = OIDCAuthenticationBackend()

@@ -662,0 +661,0 @@

@@ -1,6 +0,4 @@

from typing import Protocol
from django.contrib.auth.models import User
from django.db import IntegrityError
from django.http import HttpRequest, HttpResponseRedirect
from django.http import HttpRequest
from django.test import Client

@@ -21,13 +19,5 @@ from django.urls import reverse

from .conftest import auth_request_mark as auth_request, oidcconfig
from .factories import UserFactory
class CallbackRequestMark(Protocol):
def __call__(self, claims: JSONObject) -> pytest.MarkDecorator: ...
mock_backend_claims: CallbackRequestMark = pytest.mark.mock_backend_claims
@pytest.fixture

@@ -55,4 +45,6 @@ def mock_auth_backend(request, mocker):

@mock_backend_claims({"email": "collision@example.com", "sub": "some_username"})
@oidcconfig(
@pytest.mark.mock_backend_claims(
{"email": "collision@example.com", "sub": "some_username"}
)
@pytest.mark.oidcconfig(
enabled=True,

@@ -97,3 +89,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@mock_backend_claims(
@pytest.mark.mock_backend_claims(
{

@@ -105,3 +97,3 @@ "sub": "some_username",

)
@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -139,3 +131,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@mock_backend_claims(
@pytest.mark.mock_backend_claims(
{

@@ -146,4 +138,4 @@ "email": "nocollision@example.com",

)
@auth_request(next="/admin/")
@oidcconfig(
@pytest.mark.auth_request(next="/admin/")
@pytest.mark.oidcconfig(
enabled=True,

@@ -167,4 +159,4 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@mock_backend_claims({})
@oidcconfig(
@pytest.mark.mock_backend_claims({})
@pytest.mark.oidcconfig(
enabled=True,

@@ -225,3 +217,3 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@pytest.mark.django_db
@oidcconfig(enabled=False, oidc_op_authorization_endpoint="bad")
@pytest.mark.oidcconfig(enabled=False, oidc_op_authorization_endpoint="bad")
def test_wrong_config_model_used(

@@ -247,8 +239,7 @@ dummy_config: OIDCClient,

assert callback_response.status_code == 302
assert isinstance(callback_response, HttpResponseRedirect)
assert callback_response.url == "/admin/login/failure/"
@auth_request(next="/admin/")
@oidcconfig(
@pytest.mark.auth_request(next="/admin/")
@pytest.mark.oidcconfig(
enabled=True,

@@ -255,0 +246,0 @@ userinfo_claims_source=UserInformationClaimsSources.id_token,

@@ -8,3 +8,2 @@ """

from django.core.exceptions import DisallowedRedirect
from django.http import HttpResponseRedirect

@@ -16,4 +15,2 @@ import pytest

from .conftest import oidcconfig
pytestmark = [pytest.mark.django_db]

@@ -26,3 +23,3 @@

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -37,3 +34,2 @@ oidc_rp_client_id="fixed_client_id",

assert response.status_code == 302
assert isinstance(response, HttpResponseRedirect)
parsed_url = urlsplit(response.url)

@@ -54,3 +50,3 @@ assert parsed_url.scheme == "https"

@oidcconfig
@pytest.mark.oidcconfig()
def test_suspicious_return_url(dummy_config, auth_request):

@@ -69,3 +65,3 @@ with pytest.raises(DisallowedRedirect):

)
@oidcconfig
@pytest.mark.oidcconfig()
def test_forgotten_return_url(dummy_config, auth_request, get_kwargs):

@@ -86,5 +82,5 @@ with pytest.raises(ValueError):

@oidcconfig(check_op_availability=True)
@pytest.mark.oidcconfig(check_op_availability=True)
def test_idp_check_mechanism(dummy_config, auth_request, settings):
with pytest.raises(OIDCProviderOutage):
oidc_init_with_idp_check(auth_request)

@@ -8,3 +8,3 @@ """

from django.http import HttpRequest, HttpResponseRedirect
from django.http import HttpRequest
from django.urls import reverse

@@ -19,9 +19,8 @@

from mozilla_django_oidc_db.tests.factories import OIDCClientFactory
from mozilla_django_oidc_db.typing import GetParams
from mozilla_django_oidc_db.views import OIDCAuthenticationRequestInitView
from .conftest import auth_request_mark as auth_request, oidcconfig
@oidcconfig(oidc_op_authorization_endpoint="http://localhost:8080/openid-connect/auth")
@pytest.mark.oidcconfig(
oidc_op_authorization_endpoint="http://localhost:8080/openid-connect/auth"
)
def test_default_config_flow(filled_admin_config, client):

@@ -50,3 +49,3 @@ start_url = reverse("oidc_authentication_init")

@oidcconfig(oidc_keycloak_idp_hint="keycloak-idp2")
@pytest.mark.oidcconfig(oidc_keycloak_idp_hint="keycloak-idp2")
def test_keycloak_idp_hint_via_config(filled_admin_config, settings, client):

@@ -65,3 +64,3 @@ settings.OIDC_KEYCLOAK_IDP_HINT = "keycloak-idp1"

@oidcconfig(check_op_availability=True)
@pytest.mark.oidcconfig(check_op_availability=True)
def test_check_idp_availability_not_available(

@@ -77,3 +76,3 @@ filled_admin_config, settings, client, requests_mock

@oidcconfig(check_op_availability=True)
@pytest.mark.oidcconfig(check_op_availability=True)
def test_check_idp_availability_available(

@@ -90,4 +89,4 @@ filled_admin_config, settings, client, requests_mock

@oidcconfig(oidc_rp_scopes_list=["email"])
@auth_request
@pytest.mark.oidcconfig(oidc_rp_scopes_list=["email"])
@pytest.mark.auth_request
def test_overwrite_scope(dummy_config, auth_request):

@@ -98,6 +97,5 @@ """Test whether the scopes specified in the configuration can be overwritten."""

class OIDCTestExtraParamsPlugin(OIDCAdminPlugin):
def get_extra_params(
self, request: HttpRequest, extra_params: GetParams
) -> GetParams:
return {**extra_params, "scope": "not-email and-extra"}
def get_extra_params(self, request: HttpRequest, extra_params: dict) -> dict:
extra_params["scope"] = "not-email and-extra"
return extra_params

@@ -110,3 +108,2 @@ OIDCClientFactory.create(identifier="test-extra-scope")

assert redirect_response.status_code == 302
assert isinstance(redirect_response, HttpResponseRedirect)

@@ -119,4 +116,4 @@ parsed_url = urlsplit(redirect_response.url)

@oidcconfig()
@auth_request
@pytest.mark.oidcconfig()
@pytest.mark.auth_request
def test_override_callback_url_plugin_settings_used(dummy_config, auth_request):

@@ -138,3 +135,2 @@ @register("test-settings-override")

assert redirect_response.status_code == 302
assert isinstance(redirect_response, HttpResponseRedirect)

@@ -141,0 +137,0 @@ parsed_url = urlsplit(redirect_response.url)

@@ -8,9 +8,8 @@ from django.test import Client

from mozilla_django_oidc_db.models import OIDCClient
from mozilla_django_oidc_db.tests.utils import keycloak_login
from .conftest import oidcconfig
from .utils import keycloak_login
@pytest.mark.vcr
@oidcconfig(extra_options={"make_users_staff": True})
@pytest.mark.oidcconfig(make_users_staff=True)
def test_use_config_class_from_state_over_config_class_from_session(

@@ -17,0 +16,0 @@ keycloak_config: OIDCClient,

@@ -10,5 +10,4 @@ from django.urls import reverse

)
from mozilla_django_oidc_db.tests.utils import keycloak_login
from .conftest import oidcconfig
from .utils import keycloak_login

@@ -93,3 +92,3 @@ KEYCLOAK_BASE_URL = "http://localhost:8080/realms/test/"

@pytest.mark.vcr
@oidcconfig(
@pytest.mark.oidcconfig(
# Set up client configured to return JWT from userinfo endpoint instead of plain

@@ -121,3 +120,3 @@ # JSON. Credentials from ``docker/import`` realm export.

@pytest.mark.vcr
@oidcconfig(extra_options={"make_users_staff": True})
@pytest.mark.oidcconfig(make_users_staff=True)
def test_session_refresh(

@@ -124,0 +123,0 @@ keycloak_config,

@@ -8,6 +8,5 @@ from django.test import Client

from mozilla_django_oidc_db.models import OIDCClient
from mozilla_django_oidc_db.tests.utils import keycloak_login
from mozilla_django_oidc_db.utils import do_op_logout
from .conftest import oidcconfig
from .utils import keycloak_login

@@ -56,3 +55,3 @@

@pytest.mark.vcr
@oidcconfig(oidc_op_logout_endpoint="")
@pytest.mark.oidcconfig(oidc_op_logout_endpoint="")
def test_logout_without_endpoint_configured(

@@ -94,3 +93,3 @@ keycloak_config: OIDCClient,

@oidcconfig(oidc_op_logout_endpoint="https://example.com/oidc/logout")
@pytest.mark.oidcconfig(oidc_op_logout_endpoint="https://example.com/oidc/logout")
def test_logout_response_has_redirect(dummy_config: OIDCClient, requests_mock):

@@ -97,0 +96,0 @@ requests_mock.post(

@@ -1,2 +0,1 @@

from typing import Protocol, Unpack
from urllib.parse import parse_qs, urlparse

@@ -15,4 +14,3 @@

from .conftest import oidcconfig
from .utils import OIDCConfigOptions, create_or_update_configuration
from .utils import create_or_update_configuration

@@ -51,27 +49,22 @@

class ConfigFactory(Protocol):
def __call__(
self, config_identifier: str, /, **overrides: Unpack[OIDCConfigOptions]
) -> OIDCClient: ...
@pytest.fixture()
def config_factory(db) -> ConfigFactory:
def _factory(config_identifier: str, /, **overrides: Unpack[OIDCConfigOptions]):
def config_factory(db):
def _factory(config_identifier: str, /, **overrides):
BASE = f"https://mock-oidc-provider-{config_identifier}:9999"
config_options: OIDCConfigOptions = {
"enabled": True,
"oidc_rp_client_id": "fake",
"oidc_rp_client_secret": "even-faker",
"oidc_rp_sign_algo": "RS256",
"oidc_op_discovery_endpoint": f"{BASE}/oidc/",
"oidc_op_jwks_endpoint": f"{BASE}/oidc/jwks",
"oidc_op_authorization_endpoint": f"{BASE}/oidc/auth",
"oidc_op_token_endpoint": f"{BASE}/oidc/token",
"oidc_op_user_endpoint": f"{BASE}/oidc/user",
**overrides,
}
config = create_or_update_configuration(
f"{config_identifier}-provider", config_identifier, config_options
f"{config_identifier}-provider",
config_identifier,
{
"enabled": True,
"oidc_rp_client_id": "fake",
"oidc_rp_client_secret": "even-faker",
"oidc_rp_sign_algo": "RS256",
"oidc_op_discovery_endpoint": f"{BASE}/oidc/",
"oidc_op_jwks_endpoint": f"{BASE}/oidc/jwks",
"oidc_op_authorization_endpoint": f"{BASE}/oidc/auth",
"oidc_op_token_endpoint": f"{BASE}/oidc/token",
"oidc_op_user_endpoint": f"{BASE}/oidc/user",
**overrides,
},
)

@@ -84,3 +77,3 @@

@oidcconfig(enabled=False)
@pytest.mark.oidcconfig(enabled=False)
def test_sessionrefresh_oidc_not_enabled(

@@ -99,3 +92,3 @@ dummy_config: OIDCClient,

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -134,3 +127,3 @@ oidc_rp_client_id="initial-client-id",

@oidcconfig(enabled=True)
@pytest.mark.oidcconfig(enabled=True)
def test_sessionrefresh_config_use_defaults(

@@ -169,3 +162,3 @@ dummy_config,

config_identifier,
config_factory: ConfigFactory,
config_factory,
request_factory,

@@ -206,3 +199,3 @@ session_refresh: SessionRefresh,

config_identifier,
config_factory: ConfigFactory,
config_factory,
request_factory,

@@ -209,0 +202,0 @@ session_refresh: SessionRefresh,

@@ -7,10 +7,9 @@ import logging

from .conftest import oidcconfig
from .factories import UserFactory
@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,
username_claim=["username"],
extra_options={
"user_settings.claim_mappings.username": ["username"],
"user_settings.claim_mappings.first_name": [],

@@ -51,6 +50,6 @@ "user_settings.claim_mappings.email": ["email"],

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,
username_claim=["username"],
extra_options={
"user_settings.claim_mappings.username": ["username"],
"groups_settings.claim_mapping": ["not-present"],

@@ -57,0 +56,0 @@ },

from typing import Any
from django.contrib.sessions.backends.base import SessionBase
from django.test import RequestFactory

@@ -16,5 +15,3 @@

from .conftest import oidcconfig
@pytest.mark.parametrize(

@@ -51,3 +48,3 @@ "setting",

)
@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -74,3 +71,3 @@ oidc_rp_client_id="testid",

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -90,3 +87,3 @@ oidc_rp_client_id="testid",

@oidcconfig(
@pytest.mark.oidcconfig(
enabled=True,

@@ -104,4 +101,3 @@ oidc_rp_client_id="testid",

request = rf.get("/")
request.session = SessionBase()
request.session.update({CONFIG_IDENTIFIER_SESSION_KEY: dummy_config.identifier})
request.session = {CONFIG_IDENTIFIER_SESSION_KEY: dummy_config.identifier}

@@ -108,0 +104,0 @@ mocker.patch.object(middleware, "is_refreshable_url", return_value=True)

@@ -9,6 +9,4 @@ import pytest

from .conftest import oidcconfig
@oidcconfig(enabled=True)
@pytest.mark.oidcconfig(enabled=True)
def test_templatetag_admin_oidc_enabled(filled_admin_config):

@@ -15,0 +13,0 @@ retrieved_client = get_oidc_admin_client()

Sorry, the diff of this file is not supported yet

"""
Keycloak helpers taken from mozilla-django-oidc-db::tests/utils.py & pytest fixtures.
These help dealing with/stubbing out OpenID Provider configuration.
The Keycloak client ID/secret and URLs are set up for the config in
docker/docker-compose.keycloak.yml. See the README.md in docker/keycloak/ for more
information.
"""
from contextlib import nullcontext
from pyquery import PyQuery as pq
from requests import Session
def keycloak_login(
login_url: str,
username: str = "testuser",
password: str = "testuser",
host: str = "http://testserver/",
session: Session | None = None,
) -> str:
"""
Test helper to perform a keycloak login.
:param login_url: A login URL for keycloak with all query string parameters. E.g.
`client.get(reverse("login"))["Location"]`.
:returns: The redirect URI to consume in the django application, with the ``code``
``state`` query parameters. Consume this with ``response = client.get(url)``.
"""
cm = Session() if session is None else nullcontext(session)
with cm as session:
login_page = session.get(login_url)
assert login_page.status_code == 200
# process keycloak's login form and submit the username + password to
# authenticate
document = pq(login_page.text)
login_form = document("form#kc-form-login")
submit_url = login_form.attr("action")
assert isinstance(submit_url, str)
login_response = session.post(
submit_url,
data={
"username": username,
"password": password,
"credentialId": "",
"login": "Sign In",
},
allow_redirects=False,
)
assert login_response.status_code == 302
assert (redirect_uri := login_response.headers["Location"]).startswith(host)
return redirect_uri