Files
ebbe.bassandClaude Sonnet 5.5 3c69c2a156 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
2026-10-17 19:19:51 +02:00

42 lines
1.7 KiB
Python

"""In-memory identity provider for tests (AD-1)."""
from __future__ import annotations
import secrets
from collections.abc import Mapping
from .base import Flow, Identity, IdpUnavailable, LoginRejected
class FakeIdp:
"""`begin` hands out a flow; `complete` accepts the one-time code from `callback_params`."""
def __init__(self) -> None:
self.unavailable = False
self.identity: Identity | None = Identity("alice", "sub-alice")
self._codes: dict[str, Identity | None] = {}
def sign_in_as(self, username: str | None) -> None:
self.identity = Identity(username, f"sub-{username}") if username else None
def begin(self) -> tuple[str, Flow]:
if self.unavailable:
raise IdpUnavailable("The identity provider is not available")
flow = Flow(secrets.token_urlsafe(8), secrets.token_urlsafe(8), secrets.token_urlsafe(8))
return f"https://idp.example.test/authorize?state={flow.state}", flow
def callback_params(self, flow: Flow) -> dict[str, str]:
code = secrets.token_urlsafe(8)
self._codes[code] = self.identity
return {"state": flow.state, "code": code}
def complete(self, params: Mapping[str, str], flow: Flow) -> Identity:
if self.unavailable:
raise IdpUnavailable("The identity provider is not available")
if "error" in params or params.get("state") != flow.state:
raise LoginRejected("The sign-in response does not match the request")
identity = self._codes.pop(params.get("code", ""), None) # a code works once
if identity is None:
raise LoginRejected("The identity provider refused the code")
return identity