feat: add OIDC sign-in, roles, sessions and client address rule

Authorization-code flow with PKCE through an idp adapter (with a fake), ID
token validation, identity and roles resolved from the directory with a short
cache, refusal of unknown, disabled and locked accounts with one generic page,
sessions that end when the account becomes unavailable, sign-out that ends the
server-side session, role decorators and the trusted-proxy client address rule.
Adds the OIDC_* and PUBLIC_BASE_URL configuration.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
This commit is contained in:
2026-10-17 19:19:51 +02:00
co-authored by Claude Sonnet 5.5
parent 7458a2a815
commit 3c69c2a156
32 changed files with 1692 additions and 14 deletions
+11 -9
View File
@@ -72,7 +72,8 @@ def test_settings_defaults():
assert settings.SESSION_SAVE_EVERY_REQUEST is True
def _client_with_session(created_at):
def _client_with_session(created_at, adapters):
adapters.directory.add_user("alice")
store = SessionStore()
request = RequestFactory().get("/")
request.session = store
@@ -84,16 +85,17 @@ def _client_with_session(created_at):
return client, store.session_key
def test_absolute_limit_removes_session_and_clears_cookie():
def test_absolute_limit_removes_session_and_clears_cookie(adapters):
assert any(m.endswith("AbsoluteSessionLimitMiddleware") for m in settings.MIDDLEWARE)
client, key = _client_with_session(time.time() - settings.SESSION_ABSOLUTE_SECONDS - 5)
too_old = time.time() - settings.SESSION_ABSOLUTE_SECONDS - 5
client, key = _client_with_session(too_old, adapters)
response = client.get("/healthz")
assert not UserSession.objects.filter(session_key=key).exists()
assert response.cookies[settings.SESSION_COOKIE_NAME].value == ""
def test_session_within_limits_survives_a_request():
client, key = _client_with_session(time.time() - 60)
def test_session_within_limits_survives_a_request(adapters):
client, key = _client_with_session(time.time() - 60, adapters)
client.get("/healthz")
assert UserSession.objects.filter(session_key=key).exists()
@@ -104,8 +106,8 @@ def test_middleware_callable_directly():
assert current_user(request) is not None
def test_request_renews_expire_date_to_one_hour_ahead():
client, key = _client_with_session(time.time() - 60)
def test_request_renews_expire_date_to_one_hour_ahead(adapters):
client, key = _client_with_session(time.time() - 60, adapters)
UserSession.objects.filter(session_key=key).update(
expire_date=timezone.now() + timedelta(minutes=5)
)
@@ -114,8 +116,8 @@ def test_request_renews_expire_date_to_one_hour_ahead():
assert timedelta(minutes=59) < remaining <= timedelta(hours=1)
def test_request_with_idle_expired_session_has_no_session_and_clears_cookie():
client, key = _client_with_session(time.time() - 60)
def test_request_with_idle_expired_session_has_no_session_and_clears_cookie(adapters):
client, key = _client_with_session(time.time() - 60, adapters)
UserSession.objects.filter(session_key=key).update(
expire_date=timezone.now() - timedelta(seconds=1)
)