Allows Paskia to authenticate the user to a client site. - User friendly client registration flow on the admin app - Redirect-based authentication flow (per spec) - Backchannel logout both ways to keep sessions synchronized - Groups integrated with Paskia's permission system - Adds email, preferred username and telephone fields on user profile - All new user basic info layout to show the new information, better looks - API and DB structures redesigned - Various unrelated fixes to theming and layout
119 lines
3.6 KiB
Python
119 lines
3.6 KiB
Python
"""
|
|
OIDC Back-Channel Logout notifications.
|
|
|
|
When sessions are deleted (logout, admin, expiry), this module notifies
|
|
any OIDC clients that have a backchannel_logout_uri configured.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from uuid import UUID
|
|
|
|
import httpx
|
|
|
|
from paskia import db
|
|
from paskia.util import oidjwt
|
|
from paskia.util.hostutil import _load_config
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
# Timeout for back-channel logout requests
|
|
_TIMEOUT = httpx.Timeout(10.0, connect=5.0)
|
|
|
|
|
|
def _issuer() -> str:
|
|
"""Derive issuer URL from config (same base as discovery document)."""
|
|
cfg = _load_config()
|
|
return cfg.get("site_url", "https://localhost")
|
|
|
|
|
|
def _collect_oidc_sessions(
|
|
session_keys: list[str],
|
|
) -> list[tuple[str, str, UUID, UUID | None]]:
|
|
"""Collect (backchannel_logout_uri, sid, client_uuid, user_uuid) for OIDC sessions.
|
|
|
|
Must be called before the sessions are deleted from the database.
|
|
Returns only sessions whose client has a backchannel_logout_uri configured.
|
|
"""
|
|
notifications = []
|
|
data = db.data()
|
|
for key in session_keys:
|
|
session = data.sessions.get(key)
|
|
if not session or session.client_uuid is None:
|
|
continue
|
|
client = data.oidc.clients.get(session.client_uuid)
|
|
if not client or not client.backchannel_logout_uri:
|
|
continue
|
|
sid = session.key
|
|
notifications.append(
|
|
(client.backchannel_logout_uri, sid, session.client_uuid, session.user_uuid)
|
|
)
|
|
return notifications
|
|
|
|
|
|
async def _send_logout_token(
|
|
client: httpx.AsyncClient,
|
|
uri: str,
|
|
token: str,
|
|
) -> None:
|
|
"""POST a logout_token to a single client endpoint."""
|
|
try:
|
|
resp = await client.post(
|
|
uri,
|
|
data={"logout_token": token},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
if resp.status_code == 200:
|
|
_logger.debug("Back-channel logout OK: %s", uri)
|
|
else:
|
|
_logger.warning(
|
|
"Back-channel logout %s returned %d: %s",
|
|
uri,
|
|
resp.status_code,
|
|
resp.text[:200],
|
|
)
|
|
except Exception:
|
|
_logger.warning("Back-channel logout failed: %s", uri, exc_info=True)
|
|
|
|
|
|
async def notify(
|
|
notifications: list[tuple[str, str, UUID, UUID | None]],
|
|
) -> None:
|
|
"""Send back-channel logout tokens to all collected endpoints.
|
|
|
|
Args:
|
|
notifications: list of (backchannel_logout_uri, sid, client_uuid, user_uuid)
|
|
as returned by _collect_oidc_sessions.
|
|
"""
|
|
if not notifications:
|
|
return
|
|
|
|
issuer = _issuer()
|
|
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
|
|
tasks = []
|
|
for uri, sid, client_uuid, user_uuid in notifications:
|
|
token = oidjwt.create_logout_token(
|
|
issuer=issuer,
|
|
audience=str(client_uuid),
|
|
sid=sid,
|
|
sub=user_uuid,
|
|
)
|
|
tasks.append(_send_logout_token(client, uri, token))
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
|
|
def schedule_notifications(session_keys: list[str]) -> None:
|
|
"""Collect OIDC info from sessions (before deletion) and schedule async notifications.
|
|
|
|
Must be called BEFORE the sessions are deleted. The actual HTTP requests
|
|
are fire-and-forget via the running event loop.
|
|
"""
|
|
notifications = _collect_oidc_sessions(session_keys)
|
|
if not notifications:
|
|
return
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
loop.create_task(notify(notifications))
|
|
except RuntimeError:
|
|
_logger.debug("No event loop for back-channel logout notifications")
|