Source code for regstack.oauth.base
"""OAuth provider abstraction.
Every provider (Google in v1, GitHub / Microsoft / Apple later) is a
subclass of :class:`OAuthProvider`. The class methods correspond to
the three steps of the Authorization Code with PKCE flow:
1. :meth:`OAuthProvider.authorization_url` — what URL do we redirect
the browser to?
2. :meth:`OAuthProvider.exchange_code` — given the ``code`` the
provider hands back, get tokens.
3. :meth:`OAuthProvider.verify_id_token` — verify the ID token's
signature and claims, and pull out a canonical
:class:`OAuthUserInfo`.
The router stitches those three calls together (plus our own state /
identity bookkeeping); providers don't know about regstack's storage
layer or the FastAPI app.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
[docs]
@dataclass(frozen=True, slots=True)
class OAuthTokens:
"""Tokens returned by a provider's token-exchange endpoint."""
access_token: str
"""Bearer token usable against the provider's APIs. regstack does
not store or use this for anything other than the (rare) call to a
separate userinfo endpoint. Most providers (including Google) put
everything we need in the ID token."""
id_token: str
"""Signed JWT carrying the user's identity claims. This is the
trust anchor — its signature is verified against the provider's
JWKS."""
refresh_token: str | None = None
"""Optional refresh token. regstack does NOT use refresh tokens
for anything in v1; if set, it is discarded."""
[docs]
@dataclass(frozen=True, slots=True)
class OAuthUserInfo:
"""Canonical, provider-agnostic user information.
Each provider's :meth:`OAuthProvider.verify_id_token` produces one
of these from its native claim shape. Routers downstream see only
this — they don't care whether it came from Google's ``sub`` or
GitHub's ``id``.
"""
subject_id: str
"""The provider's stable, opaque user identifier. For Google: the
``sub`` claim. NEVER an email address — emails can change,
subjects don't."""
email: str | None
"""Optional email claim. May be ``None`` for providers that don't
always return one (some GitHub configurations)."""
email_verified: bool
"""Whether the provider considers the email verified. Auto-linking
only happens when this is ``True`` and ``email`` is non-``None``."""
full_name: str | None
"""Display name. Optional."""
picture_url: str | None
"""Avatar URL. Optional."""
[docs]
class OAuthProvider(ABC):
"""Abstract base class for OAuth / OIDC providers.
Subclasses must implement the three methods of the Authorization
Code with PKCE flow. They're free to make synchronous network
calls inside ``async`` methods (the dance involves a single
``POST`` to the token endpoint) but should not introduce any
long-running background work.
Subclasses are stateless once constructed — repeat calls don't
accumulate state — so a single instance per provider per
:class:`~regstack.app.RegStack` is the intended lifetime.
"""
@property
@abstractmethod
def name(self) -> str:
"""The lookup key for this provider in
:class:`~regstack.oauth.registry.OAuthRegistry`. Lowercase
ASCII — used in URL paths (``/oauth/{name}/start``) and in
the ``oauth_identities.provider`` column.
"""
[docs]
@abstractmethod
def authorization_url(
self,
*,
redirect_uri: str,
state: str,
code_challenge: str,
nonce: str,
) -> str:
"""Build the URL the browser should be redirected to.
Args:
redirect_uri: The full callback URL where the provider
will return the user. Must be registered with the
provider out-of-band (in Google Cloud console, etc.).
state: Opaque CSRF token. Provider returns this verbatim
in the callback. regstack uses it as the lookup key
for the in-flight :class:`OAuthState` row.
code_challenge: Base64url-encoded SHA-256 of the
``code_verifier`` (PKCE). The verifier itself stays
server-side.
nonce: Random string included in the auth request and
returned in the ID token. Defends against ID token
replay across separate authentication ceremonies.
Returns:
The URL to redirect the browser to.
"""
[docs]
@abstractmethod
async def exchange_code(
self,
*,
code: str,
redirect_uri: str,
code_verifier: str,
) -> OAuthTokens:
"""Trade an authorization code for tokens.
Args:
code: The ``code`` parameter the provider sent to the
callback.
redirect_uri: Must exactly match the ``redirect_uri``
used in :meth:`authorization_url` — the provider
refuses the exchange otherwise.
code_verifier: The PKCE pre-image whose SHA-256 was
sent as ``code_challenge`` originally. Read from
the server-side state row.
Returns:
:class:`OAuthTokens` with at least ``id_token`` and
``access_token``.
Raises:
~regstack.oauth.errors.OAuthTokenExchangeError: On any
non-200 response from the provider's token endpoint.
"""
[docs]
@abstractmethod
async def verify_id_token(
self,
id_token: str,
*,
expected_nonce: str,
) -> OAuthUserInfo:
"""Verify the ID token's signature and claims.
Concretely this checks: signature against the provider's
JWKS, ``iss`` matches the provider's issuer, ``aud`` matches
the configured client_id, ``exp`` is in the future,
``nonce`` matches ``expected_nonce``.
Args:
id_token: The ID token from :class:`OAuthTokens`.
expected_nonce: The nonce the auth request was made with,
stored on the state row.
Returns:
:class:`OAuthUserInfo` distilled from the provider's
native claim shape.
Raises:
~regstack.oauth.errors.OAuthIdTokenError: If any check
fails. The exception message is suitable for
logging; do NOT echo it to the end user — it could
leak which check failed and help an attacker craft a
better forgery.
"""