Skip to content

Commit 8b575dc

Browse files
committed
Settings rework: stable status.
1 parent 83e3f7f commit 8b575dc

37 files changed

+1229
-732
lines changed

DEVELOPMENT.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ sphinx-autobuild docs docs/_build/html
2222

2323
## Running Tests
2424

25-
Check database settings in tests/test_settings.py, target a real PostgreSQL Host (You need a PostgreSQL version 12 or greater).
25+
Check database settings in `tests/test_settings.py`, target a real PostgreSQL Host (You need a PostgreSQL version 12 or greater), for e2e tests check the `tests/e2e/settings.py` file.
2626

2727
```
28-
python3 runtests.py
28+
python3 run_tests.py # for unit tests
29+
python3 run_e2e_tests.py # for end to end tests
2930
```
3031

3132
## Adding a dependency

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,22 @@ Now you can pick an identity provider from the [available providers](https://dja
9090

9191
Create a file named `oidc.py` next to your settings file and initialize your provider there :
9292

93+
FIXME: Here config as settings only OR using custom provider
94+
9395
```python
9496
from django_pyoidc.providers.keycloak import KeycloakProvider
9597

9698
my_oidc_provider = KeycloakProvider(
9799
op_name="keycloak",
98-
client_secret="s3cret",
99-
client_id="my_client_id",
100100
keycloak_base_uri="http://keycloak.local:8080/auth/", # we use the auth/ path prefix option on Keycloak
101101
keycloak_realm="Demo",
102+
client_secret="s3cret",
103+
client_id="my_client_id",
102104
logout_redirect="http://app.local:8082/",
103105
failure_redirect="http://app.local:8082/",
104106
success_redirect="http://app.local:8082/",
105107
redirect_requires_https=False,
108+
login_uris_redirect_allowed_hosts=["app.local:8082"],
106109
)
107110
```
108111

@@ -112,7 +115,7 @@ You can then add to your django configuration the following line :
112115
from .oidc_providers import my_oidc_provider
113116

114117
DJANGO_PYOIDC = {
115-
**my_oidc_provider.get_config(allowed_hosts=["app.local:8082"]),
118+
**my_oidc_provider.get_config(),
116119
}
117120
```
118121

django_pyoidc/client.py

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,39 @@
55
from oic.oic.consumer import Consumer
66
from oic.utils.authn.client import CLIENT_AUTHN_METHOD
77

8+
from django_pyoidc.exceptions import (
9+
InvalidOIDCConfigurationException,
10+
InvalidSIDException,
11+
)
812
from django_pyoidc.session import OIDCCacheSessionBackendForDjango
913
from django_pyoidc.settings import OIDCSettingsFactory
10-
from django_pyoidc.utils import OIDCCacheBackendForDjango, get_setting_for_sso_op
14+
from django_pyoidc.utils import OIDCCacheBackendForDjango
1115

1216
logger = logging.getLogger(__name__)
1317

1418

1519
class OIDCClient:
16-
def __init__(self, op_name, session_id=None):
17-
self._op_name = op_name
18-
self.settings = OIDCSettingsFactory.get(self.op_name)
20+
def __init__(self, op_name: str, session_id=None):
21+
self.opsettings = OIDCSettingsFactory.get(op_name)
1922

20-
self.session_cache_backend = OIDCCacheSessionBackendForDjango(self._op_name)
21-
self.general_cache_backend = OIDCCacheBackendForDjango(self._op_name)
22-
23-
consumer_config = {
24-
# "debug": True,
25-
"response_type": "code"
26-
}
23+
self.session_cache_backend = OIDCCacheSessionBackendForDjango(self.opsettings)
24+
self.general_cache_backend = OIDCCacheBackendForDjango(self.opsettings)
25+
client_id = self.opsettings.get("client_id")
26+
client_secret = self.opsettings.get("client_secret", None)
27+
consumer_config = self.opsettings.get(
28+
"client_consumer_config_dict",
29+
{
30+
# "debug": True,
31+
"response_type": "code"
32+
},
33+
)
2734

2835
client_config = {
29-
"client_id": get_setting_for_sso_op(op_name, "OIDC_CLIENT_ID"),
30-
"client_authn_method": CLIENT_AUTHN_METHOD,
36+
"client_id": client_id,
37+
"client_authn_method": self.opsettings.get(
38+
"client_authn_method", CLIENT_AUTHN_METHOD
39+
),
3140
}
32-
3341
self.consumer = Consumer(
3442
session_db=self.session_cache_backend,
3543
consumer_config=consumer_config,
@@ -38,24 +46,40 @@ def __init__(self, op_name, session_id=None):
3846
# used in token introspection
3947
self.client_extension = ClientExtension(**client_config)
4048

41-
provider_info_uri = get_setting_for_sso_op(
42-
op_name, "OIDC_PROVIDER_DISCOVERY_URI"
43-
)
44-
client_secret = get_setting_for_sso_op(op_name, "OIDC_CLIENT_SECRET")
49+
provider_discovery_uri = self.opsettings.get("provider_discovery_uri", None)
4550
self.client_extension.client_secret = client_secret
4651

4752
if session_id:
48-
self.consumer.restore(session_id)
49-
else:
50-
51-
cache_key = self.general_cache_backend.generate_hashed_cache_key(
52-
provider_info_uri
53-
)
5453
try:
55-
config = self.general_cache_backend[cache_key]
54+
self.consumer.restore(session_id)
5655
except KeyError:
57-
config = self.consumer.provider_config(provider_info_uri)
58-
# shared microcache for provider config
59-
# FIXME: Setting for duration
60-
self.general_cache_backend.set(cache_key, config, 60)
61-
self.consumer.client_secret = client_secret
56+
# This is an error as for example during the first communication round trips between
57+
# the op and the client we'll have to find state elements in the oidc session
58+
raise InvalidSIDException(
59+
f"OIDC consumer failed to restore oidc session {session_id}."
60+
)
61+
return
62+
63+
if not provider_discovery_uri:
64+
raise InvalidOIDCConfigurationException(
65+
"No provider discovery uri provided."
66+
)
67+
else:
68+
if self.opsettings.get("oidc_cache_provider_metadata", False):
69+
cache_key = self.general_cache_backend.generate_hashed_cache_key(
70+
provider_discovery_uri
71+
)
72+
try:
73+
config = self.general_cache_backend[cache_key]
74+
# this will for example register endpoints on the consumer object
75+
self.consumer.handle_provider_config(config, provider_discovery_uri)
76+
except KeyError:
77+
# This make an HTTP call on provider discovery uri
78+
config = self.consumer.provider_config(provider_discovery_uri)
79+
# shared microcache for provider config
80+
# FIXME: Setting for duration
81+
self.general_cache_backend.set(cache_key, config, 60)
82+
else:
83+
# This make an HTTP call on provider discovery uri
84+
config = self.consumer.provider_config(provider_discovery_uri)
85+
self.consumer.client_secret = client_secret

django_pyoidc/drf/authentication.py

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
import functools
22
import logging
33

4-
from django.conf import settings
54
from django.core.exceptions import PermissionDenied
65
from rest_framework import exceptions
76
from rest_framework.authentication import BaseAuthentication
87

98
from django_pyoidc.client import OIDCClient
109
from django_pyoidc.engine import OIDCEngine
11-
from django_pyoidc.utils import (
12-
OIDCCacheBackendForDjango,
13-
check_audience,
14-
get_setting_for_sso_op,
15-
)
10+
from django_pyoidc.settings import OIDCSettingsFactory
11+
from django_pyoidc.utils import OIDCCacheBackendForDjango, check_audience
1612

1713
logger = logging.getLogger(__name__)
1814

@@ -24,38 +20,13 @@ class OidcAuthException(Exception):
2420
class OIDCBearerAuthentication(BaseAuthentication):
2521
def __init__(self, *args, **kwargs):
2622
super(OIDCBearerAuthentication, self).__init__(*args, **kwargs)
27-
self.op_name = self.extract_drf_opname()
28-
self.general_cache_backend = OIDCCacheBackendForDjango(self.op_name)
29-
self.engine = OIDCEngine(self.op_name)
23+
self.opsettings = OIDCSettingsFactory.get("drf")
24+
self.general_cache_backend = OIDCCacheBackendForDjango(self.opsettings)
25+
self.engine = OIDCEngine(self.opsettings)
3026

3127
@functools.cached_property
3228
def client(self):
33-
return OIDCClient(self.op_name)
34-
35-
@classmethod
36-
def extract_drf_opname(cls):
37-
"""
38-
Given a list of opnames and setting in DJANGO_PYOIDC conf, extract the one having USED_BY_REST_FRAMEWORK=True.
39-
"""
40-
op = None
41-
found = False
42-
for op_name, configs in settings.DJANGO_PYOIDC.items():
43-
if (
44-
"USED_BY_REST_FRAMEWORK" in configs
45-
and configs["USED_BY_REST_FRAMEWORK"]
46-
):
47-
if found:
48-
raise RuntimeError(
49-
"Several DJANGO_PYOIDC sections are declared as USED_BY_REST_FRAMEWORK, only one should be used."
50-
)
51-
found = True
52-
op = op_name
53-
if found:
54-
return op
55-
else:
56-
raise RuntimeError(
57-
"No DJANGO_PYOIDC sections are declared with USED_BY_REST_FRAMEWORK configuration option."
58-
)
29+
return OIDCClient("drf")
5930

6031
def extract_access_token(self, request) -> str:
6132
val = request.headers.get("Authorization")
@@ -64,11 +35,9 @@ def extract_access_token(self, request) -> str:
6435
raise OidcAuthException(msg)
6536
val = val.strip()
6637
bearer_name, access_token_jwt = val.split(maxsplit=1)
67-
requested_bearer_name = get_setting_for_sso_op(
68-
self.op_name, "OIDC_API_BEARER_NAME", "Bearer"
69-
)
38+
requested_bearer_name = self.opsettings.get("oidc_api_bearer_name", "Bearer")
7039
if not bearer_name.lower() == requested_bearer_name.lower():
71-
msg = f"Bad authorization header, invalid Keyword for the bearer, expecting {requested_bearer_name}."
40+
msg = f"Bad authorization header, invalid Keyword for the bearer, expecting {requested_bearer_name} (check setting oidc_api_bearer_name)."
7241
raise OidcAuthException(msg)
7342
return access_token_jwt
7443

@@ -104,7 +73,7 @@ def authenticate(self, request):
10473
logger.debug("Request has valid access token.")
10574

10675
# FIXME: Add a setting to disable
107-
client_id = get_setting_for_sso_op(self.op_name, "OIDC_CLIENT_ID")
76+
client_id = self.opsettings.get("client_id")
10877
if not check_audience(client_id, access_token_claims):
10978
raise PermissionDenied(
11079
f"Invalid result for acces token audiences check for {client_id}."

django_pyoidc/drf/schema.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import logging
2-
from urllib.parse import urljoin
32

43
logger = logging.getLogger(__name__)
54

65
try:
76
from drf_spectacular.extensions import OpenApiAuthenticationExtension
87

9-
from django_pyoidc.utils import get_setting_for_sso_op
8+
from django_pyoidc.settings import OIDCSettingsFactory
109

1110
class OIDCScheme(OpenApiAuthenticationExtension):
1211
target_class = "django_pyoidc.drf.authentication.OIDCBearerAuthentication"
@@ -15,21 +14,15 @@ class OIDCScheme(OpenApiAuthenticationExtension):
1514
priority = -1
1615

1716
def get_security_definition(self, auto_schema):
18-
from django_pyoidc.drf.authentication import OIDCBearerAuthentication
19-
20-
op = OIDCBearerAuthentication.extract_drf_opname()
21-
well_known_url = get_setting_for_sso_op(op, "OIDC_PROVIDER_DISCOVERY_URI")
22-
if not well_known_url.endswith(".well-known/openid-configuration"):
23-
if not well_known_url.endswith("/"):
24-
well_known_url += "/"
25-
well_known_url = urljoin(
26-
well_known_url, ".well-known/openid-configuration"
27-
)
17+
# from django_pyoidc.drf.authentication import OIDCBearerAuthentication
18+
19+
opsettings = OIDCSettingsFactory.get("drf")
20+
well_known_url = opsettings.get("provider_discovery_uri")
2821

29-
header_name = get_setting_for_sso_op(op, "OIDC_API_BEARER_NAME", "Bearer")
22+
header_name = opsettings.get("oidc_api_bearer_name", "Bearer")
3023
if header_name != "Bearer":
3124
logger.warning(
32-
"The configuration for 'OIDC_API_BEARER_NAME' will cause issue with swagger UI :"
25+
"The configuration for 'oidc_api_bearer_name' will cause issue with swagger UI :"
3326
"it is not yet possible to change the header name for swagger UI, you should stick to"
3427
"'Bearer'."
3528
)

django_pyoidc/engine.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,50 @@
33

44
from django_pyoidc import get_user_by_email
55
from django_pyoidc.client import OIDCClient
6-
from django_pyoidc.utils import (
7-
OIDCCacheBackendForDjango,
8-
get_setting_for_sso_op,
9-
import_object,
10-
)
6+
from django_pyoidc.settings import OIDCSettings
7+
from django_pyoidc.utils import OIDCCacheBackendForDjango, import_object
118

129
logger = logging.getLogger(__name__)
1310

1411

1512
class OIDCEngine:
16-
def __init__(self, op_name: str):
17-
self.op_name = op_name
18-
self.general_cache_backend = OIDCCacheBackendForDjango(self.op_name)
13+
def __init__(self, opsettings: OIDCSettings):
14+
self.opsettings = opsettings
15+
self.general_cache_backend = OIDCCacheBackendForDjango(opsettings)
1916

20-
def call_function(self, setting_name, *args, **kwargs):
21-
function_path = get_setting_for_sso_op(self.op_name, setting_name)
17+
def call_function(self, setting_func_name, *args, **kwargs):
18+
function_path = self.opsettings.get(setting_func_name)
2219
if function_path:
2320
func = import_object(function_path, "")
2421
return func(*args, **kwargs)
2522

2623
def call_get_user_function(self, tokens={}):
27-
if get_setting_for_sso_op(self.op_name, "HOOK_GET_USER"):
24+
if self.opsettings.get("HOOK_GET_USER"):
2825
logger.debug("OIDC, Calling user hook on get_user")
2926
return self.call_function("HOOK_GET_USER", tokens)
3027
else:
3128
return get_user_by_email(tokens)
3229

3330
def introspect_access_token(self, access_token_jwt: str, client: OIDCClient):
3431
"""
35-
Perform a cached intropesction call to extract claims from encoded jwt of the access_token
32+
Perform a cached introspection call to extract claims from encoded jwt of the access_token
3633
"""
3734
# FIXME: allow a non-cached mode by global settings
3835
access_token_claims = None
3936

40-
# FIXME: in what case could we not have an access token available?
37+
# FIXME: in what case could we do not have an access token available?
4138
# should we raise an error then?
4239
if access_token_jwt is not None:
4340
cache_key = self.general_cache_backend.generate_hashed_cache_key(
4441
access_token_jwt
4542
)
4643
try:
47-
access_token_claims = self.general_cache_backend["cache_key"]
44+
access_token_claims = self.general_cache_backend[cache_key]
4845
except KeyError:
4946
# CACHE MISS
5047

5148
# RFC 7662: token introspection: ask SSO to validate and render the jwt as json
52-
# this means a slow web call
49+
# this means a slow http call
5350
request_args = {
5451
"token": access_token_jwt,
5552
"token_type_hint": "access_token",

django_pyoidc/providers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from .keycloak_18 import Keycloak18Provider # noqa
55
from .lemonldapng import LemonLDAPngProvider # noqa
66
from .lemonldapng2 import LemonLDAPng2Provider # noqa
7+
from .provider import Provider # noqa

0 commit comments

Comments
 (0)