"""Loopback identity, directory and mailbox double for communication journeys. This is a synthetic test peer. It does not send SMTP, store a reusable password, or grant application membership from a mailbox event. """ from __future__ import annotations import base64 import hashlib import json import secrets import threading import time from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import parse_qs, urlencode, urlparse CLIENT_ID = "coulomb-social-test" TENANT = "tenant:communication-test" ALLOWED_CALLER = "coulomb-social-test" def challenge_s256(verifier: str) -> str: digest = hashlib.sha256(verifier.encode("ascii")).digest() return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") def _html(body: str, *, status: int = 200) -> tuple[int, str, bytes, list[tuple[str, str]]]: page = ( "Isolated issuer" f"
{body}
" ) return status, "text/html; charset=utf-8", page.encode(), [] def _esc(value: str) -> str: return ( value.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """) ) class IsolatedIssuer: def __init__(self) -> None: from authlib.jose import JsonWebKey self.key = JsonWebKey.generate_key("RSA", 2048, is_private=True) self.allowed_redirects: set[str] = set() self.codes: dict[str, dict] = {} self.messages: list[dict] = [] self.password_hashes: dict[str, str] = {} self._salt = secrets.token_hex(16) self._httpd: ThreadingHTTPServer | None = None self._thread: threading.Thread | None = None self.port = 0 @property def base(self) -> str: return f"http://127.0.0.1:{self.port}" def allow_redirect(self, url: str) -> None: self.allowed_redirects.add(url) def start(self) -> None: issuer = self class Handler(BaseHTTPRequestHandler): def do_GET(self) -> None: # noqa: N802 self._dispatch("GET") def do_POST(self) -> None: # noqa: N802 self._dispatch("POST") def log_message(self, format: str, *args) -> None: return def _dispatch(self, method: str) -> None: parsed = urlparse(self.path) form = self._form() if method == "POST" else {} query = {key: values[-1] for key, values in parse_qs(parsed.query).items()} status, content_type, body, headers = issuer.handle( method, parsed.path, query, form ) self.send_response(status) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(body))) for name, value in headers: self.send_header(name, value) self.end_headers() self.wfile.write(body) def _form(self) -> dict[str, str]: length = int(self.headers.get("Content-Length") or "0") raw = self.rfile.read(length).decode() if length else "" return {key: values[-1] for key, values in parse_qs(raw).items()} self._httpd = ThreadingHTTPServer(("127.0.0.1", 0), Handler) self.port = self._httpd.server_address[1] self._thread = threading.Thread(target=self._httpd.serve_forever, daemon=True) self._thread.start() def stop(self) -> None: if self._httpd is not None: self._httpd.shutdown() self._httpd.server_close() if self._thread is not None: self._thread.join(timeout=5) def remember_password(self, subject: str, password: str) -> None: digest = hashlib.sha256(f"{self._salt}|{subject}|{password}".encode()).hexdigest() self.password_hashes[subject] = digest def handle( self, method: str, path: str, query: dict[str, str], form: dict[str, str], ) -> tuple[int, str, bytes, list[tuple[str, str]]]: routes = { ("GET", "/.well-known/openid-configuration"): self._discovery_response, ("GET", "/jwks"): self._jwks, ("GET", "/authorize"): lambda: self._authorize("GET", query, form), ("POST", "/authorize"): lambda: self._authorize("POST", query, form), ("POST", "/token"): lambda: self._token(form), ("GET", "/userinfo"): self._userinfo, ("GET", "/recover"): lambda: self._recover("GET", query, form), ("POST", "/recover"): lambda: self._recover("POST", query, form), ("GET", "/mailbox"): lambda: _html(self._mailbox_body()), ("POST", "/recover/consume"): lambda: self._consume(form, kind="reset"), ("POST", "/recover/cancel"): lambda: self._cancel(form), ("POST", "/recover/password"): lambda: self._password(form), ("GET", "/register"): lambda: self._register("GET", form), ("POST", "/register"): lambda: self._register("POST", form), ("POST", "/verify/consume"): lambda: self._consume(form, kind="verification"), ("POST", "/verify/cancel"): lambda: self._cancel(form), ("GET", "/invite"): lambda: self._invite("GET", form), ("POST", "/invite"): lambda: self._invite("POST", form), } route = routes.get((method, path)) if route is None: return _html("

Not found

", status=404) return route() def _discovery(self) -> dict: return { "issuer": self.base, "authorization_endpoint": f"{self.base}/authorize", "token_endpoint": f"{self.base}/token", "userinfo_endpoint": f"{self.base}/userinfo", "jwks_uri": f"{self.base}/jwks", "response_types_supported": ["code"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"], } def _discovery_response(self) -> tuple: body = json.dumps(self._discovery()).encode() return 200, "application/json", body, [] def _jwks(self) -> tuple: body = json.dumps({"keys": [self.key.as_dict(is_private=False)]}).encode() return 200, "application/json", body, [] def _authorize(self, method: str, query: dict, form: dict) -> tuple: fields = form if method == "POST" else query client_id = fields.get("client_id", "") redirect_uri = fields.get("redirect_uri", "") if client_id != CLIENT_ID: return _html("

Client refused

", status=400) if redirect_uri not in self.allowed_redirects: return _html("

Redirect refused

", status=400) challenge = fields.get("code_challenge", "") if not challenge or fields.get("code_challenge_method") != "S256": return _html("

PKCE required

", status=400) if method == "GET": return _html(self._authorize_form(fields)) subject = (fields.get("subject") or "").strip() email = (fields.get("email") or "").strip() tenant = (fields.get("tenant") or "").strip() name = (fields.get("name") or subject).strip() if tenant != TENANT: return _html("

Tenant refused

", status=400) if subject == "member-recovered" and subject not in self.password_hashes: return _html("

Recovery required

", status=400) if not subject or not email: return _html("

Subject and email are required

", status=400) code = secrets.token_urlsafe(18) self.codes[code] = { "challenge": challenge, "redirect_uri": redirect_uri, "client_id": client_id, "subject": subject, "email": email, "name": name, "tenant": tenant, "expired": fields.get("expired") == "1", "used": False, } location = redirect_uri + ("&" if "?" in redirect_uri else "?") + urlencode( {"code": code, "state": fields.get("state", "")} ) return 302, "text/plain", b"", [("Location", location)] def _authorize_form(self, fields: dict[str, str]) -> str: hidden = "".join( f'' for key, value in fields.items() if key not in {"subject", "email", "name", "tenant", "expired"} ) return ( "

Sign in at the isolated issuer

" '
' f"{hidden}" '' '' '' f'' '' "
" ) def _token(self, form: dict[str, str]) -> tuple: code = self.codes.get(form.get("code", "")) verifier = form.get("code_verifier", "") if ( form.get("grant_type") != "authorization_code" or code is None or code["used"] or form.get("client_id") != code["client_id"] or form.get("redirect_uri") != code["redirect_uri"] or challenge_s256(verifier) != code["challenge"] ): body = json.dumps({"error": "invalid_grant"}).encode() return 400, "application/json", body, [] code["used"] = True now = int(time.time()) exp = now - 120 if code["expired"] else now + 300 claims = { "iss": self.base, "sub": code["subject"], "aud": CLIENT_ID, "exp": exp, "iat": now - 10, "email": code["email"], "name": code["name"], "tenant": code["tenant"], "azp": CLIENT_ID, } from authlib.jose import jwt kid = self.key.as_dict(is_private=False).get("kid") token = jwt.encode({"alg": "RS256", "kid": kid}, claims, self.key).decode() access = secrets.token_urlsafe(18) body = json.dumps( { "access_token": access, "token_type": "Bearer", "expires_in": 300, "id_token": token, } ).encode() return 200, "application/json", body, [] def _userinfo(self) -> tuple: return 200, "application/json", b"{}", [] def _add_message(self, *, kind: str, email: str, subject: str, expired: bool) -> None: self.messages.append( { "kind": kind, "email": email, "subject": subject, "handle": secrets.token_urlsafe(18), "expired": expired, "state": "ready", } ) def _recover(self, method: str, query: dict, form: dict) -> tuple: if method == "GET": expired = "1" if query.get("expired") == "1" else "0" return _html( "

Request a reset

" '
' f'' '' '
' ) email = (form.get("email") or "").strip() subject = "member-recovered" if email == "member@example.test" else "other-recovered" self._add_message( kind="reset", email=email, subject=subject, expired=form.get("expired") == "1" ) return _html( "

Reset message is in the local mailbox

Open mailbox" ) def _mailbox_body(self) -> str: parts = ["

Local mailbox

"] if not self.messages: parts.append("

No messages.

") for message in self.messages: label = message["kind"] if message["state"] != "ready": parts.append(f"

{_esc(label)} {_esc(message['state'])}

") continue if label == "invitation": parts.append( f"

{_esc(message['email'])} invitation

" "" ) continue action = "/recover/consume" if label == "reset" else "/verify/consume" cancel = "/recover/cancel" if label == "reset" else "/verify/cancel" button = "Use expired reset" if message["expired"] else f"Use {label}" handle = _esc(message["handle"]) parts.append( f'
' f'' f"
" f'
' f'' f"
" ) return "".join(parts) def _message(self, handle: str) -> dict | None: for message in self.messages: if message["handle"] == handle: return message return None def _consume(self, form: dict, *, kind: str) -> tuple: message = self._message(form.get("handle", "")) if message is None or message["kind"] != kind: return _html("

Unknown handle

", status=400) if message["state"] != "ready": return _html("

Handle already used

", status=400) if message["expired"]: message["state"] = "expired" return _html("

Handle expired

", status=400) message["state"] = "consumed" if kind == "reset": return _html( "

Set a new password

" '
' f'' '' "
" "

The application never receives this password.

" ) return _html( "

Verification consumed

" "

This does not sign in to the application.

" ) def _password(self, form: dict) -> tuple: subject = (form.get("subject") or "").strip() password = form.get("password") or "" if not subject or not password: return _html("

Password required

", status=400) self.remember_password(subject, password) return _html("

Password updated

") def _cancel(self, form: dict) -> tuple: message = self._message(form.get("handle", "")) if message is None or message["state"] != "ready": return _html("

Handle already used

", status=400) message["state"] = "cancelled" return _html("

Handle cancelled

") def _register(self, method: str, form: dict) -> tuple: if method == "GET": return _html( "

Registration sink

" "
" '' "
" ) self._add_message( kind="verification", email=(form.get("email") or "").strip(), subject="applicant", expired=False, ) return _html( "

Verification message is in the local mailbox

" 'Open mailbox' ) def _invite(self, method: str, form: dict) -> tuple: if method == "GET": return _html( "

Invite a member

" "
" '' '' '' "
" ) if form.get("caller") != ALLOWED_CALLER: return _html("

Caller refused

", status=400) if form.get("suppress") == "1": return _html("

Invitation suppressed

") self._add_message( kind="invitation", email=(form.get("email") or "").strip(), subject="invitee", expired=False, ) return _html('

Invitation recorded

Open mailbox')