"""The identity-provider interface, its data types and typed errors (AD-1, AD-17).""" from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass from typing import Protocol class IdpError(Exception): """Base class of every error the identity-provider adapter raises.""" class IdpUnavailable(IdpError): """The provider could not be reached or answered with something unusable.""" class LoginRejected(IdpError): """The sign-in response is not valid: wrong state, bad token, provider error.""" @dataclass(frozen=True) class Flow: """What must survive between the redirect and the callback. Kept in the session.""" state: str nonce: str code_verifier: str def as_dict(self) -> dict[str, str]: return {"state": self.state, "nonce": self.nonce, "code_verifier": self.code_verifier} @classmethod def from_dict(cls, data: Mapping[str, str]) -> Flow: return cls(data["state"], data["nonce"], data["code_verifier"]) @dataclass(frozen=True) class Identity: username: str # the validated preferred_username claim subject: str class Idp(Protocol): def begin(self) -> tuple[str, Flow]: """The address to send the browser to, and the flow to remember.""" def complete(self, params: Mapping[str, str], flow: Flow) -> Identity: """Validate the callback's query parameters against the flow; return who signed in."""