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:
@@ -0,0 +1,41 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user