Source code for regstack.models.oauth_state
"""Server-side state row for an in-flight OAuth flow.
PKCE's ``code_verifier`` must stay server-side; we keep it (plus the
target redirect, the flow mode, and the linking user when applicable)
in this row, addressed by a random ``id``. The OAuth ``state``
parameter the browser carries is just that ID.
After the callback completes, the same row is updated with
``result_token`` (the session JWT). The SPA exchanges its ID for the
token via ``POST /oauth/exchange``; the row is deleted on first
exchange. Anything still sitting around past ``expires_at`` is
reaped.
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
from regstack.models._objectid import IdStr
def _utcnow() -> datetime:
return datetime.now(UTC)
def _five_min_hence() -> datetime:
return _utcnow() + timedelta(minutes=5)
OAuthFlowMode = Literal["signin", "link"]
[docs]
class OAuthState(BaseModel):
"""One row per in-flight OAuth flow."""
model_config = ConfigDict(populate_by_name=True, extra="allow")
id: IdStr = Field(alias="_id")
"""The state-parameter value the browser carries through Google.
Always supplied by the caller; this is the lookup key."""
provider: str
"""Provider name — matches ``OAuthProvider.name``."""
code_verifier: str
"""PKCE pre-image. Never leaves the server."""
nonce: str
"""Random string echoed in the ID token. Defends against ID-token
replay across separate authentication ceremonies."""
redirect_to: str
"""Where the SPA should land after the OAuth flow completes.
Validated against ``config.base_url`` at /start time so the
callback can't be coerced into an open redirect."""
mode: OAuthFlowMode
"""``"signin"`` for unauthenticated start, ``"link"`` for adding a
provider to a logged-in account."""
linking_user_id: IdStr | None = None
"""Set on ``mode = "link"`` flows. The user the new identity gets
attached to. Captured at /start time from the bearer token, so the
callback (which has no auth header) doesn't have to re-authenticate."""
created_at: datetime = Field(default_factory=_utcnow)
expires_at: datetime = Field(default_factory=_five_min_hence)
"""TTL boundary. Mongo's ``expireAfterSeconds`` reaps rows; SQL
backends rely on read-side ``expires_at > now()`` plus the optional
``purge_expired`` reaper."""
result_token: str | None = None
"""Populated on a successful callback. The SPA exchanges its
``id`` for this value via ``POST /oauth/exchange``; the row is
deleted on exchange."""
[docs]
def to_mongo(self) -> dict[str, Any]:
data = self.model_dump(by_alias=True, exclude_none=True)
return data