Authentication API¶
The OAuth2 layer: token models, the store protocol and its backends, the headless code-exchange/refresh functions, the provider that clients consume, and the login callback listener.
anafpy.auth.models ¶
Token model.
ANAF access tokens are JWTs valid 90 days; refresh tokens 365 days. Refresh rotates the refresh token (a new access and refresh token come back), so both are persisted. Expiry timestamps are computed from the JWTs themselves (cached per instance), so the persisted store can never drift from the tokens it holds.
TokenSet ¶
Bases: BaseModel
An access/refresh token pair; expiries are derived from the JWT exp claims.
Instances are treated as immutable: refresh rotation builds a new TokenSet
via :meth:from_token_response, which is what makes the cached expiry properties
safe.
access_expires_at
cached
property
¶
access_expires_at: float
Access expiry (epoch s): JWT exp, else expires_in, else 90 days.
refresh_expires_at
cached
property
¶
refresh_expires_at: float
Refresh expiry (epoch s): JWT exp, else the documented 365 days.
from_token_response
classmethod
¶
from_token_response(data: dict[str, Any], *, obtained_at: float | None = None) -> TokenSet
Build from ANAF's JSON token response (access_token, refresh_token).
access_expired ¶
access_expired(*, leeway: float = 300.0) -> bool
True if the access token is expired (or within leeway seconds of it).
refresh_expired ¶
refresh_expired() -> bool
True if the refresh token has likely expired (re-auth required).
anafpy.auth.store ¶
Token persistence.
TokenStore is the abstraction the rest of the library depends on;
KeyringTokenStore is the default backend — tokens live in the OS credential store
(macOS Keychain, Windows Credential Manager, Linux Secret Service/KWallet).
FileTokenStore (a JSON file with owner-only permissions, suitable for mounting as
a Docker volume) is the opt-out for headless hosts without a credential store.
TokenStore ¶
Bases: Protocol
Where token sets are loaded from and saved to.
MemoryTokenStore ¶
MemoryTokenStore(tokens: TokenSet | None = None)
In-process token store (tests, ephemeral runtimes).
FileTokenStore ¶
FileTokenStore(path: str | PathLike[str])
JSON-file token store, written atomically with 0o600 permissions.
load ¶
load() -> TokenSet | None
The stored token set, or None when no store file exists.
Raises:
| Type | Description |
|---|---|
AnafConfigError
|
the file exists but cannot be read or parsed — stay in the AnafError hierarchy instead of leaking a raw pydantic/OS error. |
clear ¶
clear() -> None
Delete the store file; a no-op when none exists.
Raises:
| Type | Description |
|---|---|
AnafConfigError
|
the file exists but cannot be removed. |
KeyringTokenStore ¶
KeyringTokenStore(service: str = 'anafpy', username: str = 'tokens', *, chunk_size: int | None = None)
Token store in the OS credential store, via the keyring library.
The default backend. Uses the platform's native secret store — macOS Keychain,
Windows Credential Manager, or a Linux Secret Service/KWallet daemon. The token
set is stored as JSON under (service, username); a set longer than
chunk_size characters is split across continuation entries (username#1,
#2, ...), which is what makes Windows work at all (its blob cap is smaller
than one ANAF JWT). chunk_size=None picks the platform default: split on
Windows, one entry everywhere else.
Construction fails with :class:~anafpy.exceptions.AnafConfigError when no
usable OS backend exists (e.g. headless Linux without a Secret Service
daemon) — use the file backend there.
load ¶
load() -> TokenSet | None
The stored token set, or None when the credential store has none.
Raises:
| Type | Description |
|---|---|
AnafConfigError
|
entries exist but cannot be read or parsed — stay in the AnafError hierarchy instead of leaking a keyring/pydantic error. |
clear ¶
clear() -> None
Delete the token entries (including continuation chunks); no-op when absent.
Raises:
| Type | Description |
|---|---|
AnafConfigError
|
the credential store cannot be read or written. |
anafpy.auth.oauth ¶
ANAF OAuth2 endpoints and operations.
Verified against the official OAuth procedure PDF and a live probe (2026-06-28):
client auth is HTTP Basic (client_id:client_secret); token_content_type is
jwt (query for /authorize, body for /token); refresh is headless (no
certificate). See docs/anaf-reference/oauth/authentication.md.
build_authorize_url ¶
build_authorize_url(client_id: str, redirect_uri: str, *, state: str | None = None) -> str
Build the browser authorization URL (the cert step happens here).
anafpy.auth.provider ¶
Token lifecycle: hand out a valid access token, refreshing transparently.
TokenProvider is the batteries-included implementation over a TokenStore.
AnafAuth plugs it into httpx so every request carries a Bearer token and a 401
triggers a single refresh-and-retry.
TokenProvider ¶
TokenProvider(*, client_id: str, client_secret: str, store: TokenStore, http: AsyncClient | None = None)
Provides valid access tokens, refreshing and persisting as needed.
The TokenStore is the single source of truth: the provider keeps no token
state of its own. Every operation reads the freshest persisted set under the
lock, so a login or a refresh-token rotation performed by another process
sharing the store (the CLI, a second server) is picked up on the next call.
tokens
property
¶
tokens: TokenSet | None
The persisted token set, if authenticated (read-only snapshot).
access_token
async
¶
access_token() -> str
Return a currently-valid access token, refreshing if expired.
force_refresh
async
¶
force_refresh(*, stale: str | None = None) -> str
Refresh unconditionally (used after a 401); return the new access token.
Pass the access token the failing request carried as stale: when it has
already been replaced (a concurrent request or another process refreshed
first), the current token is returned without burning another rotation.
AnafAuth ¶
AnafAuth(provider: TokenProvider)
Bases: Auth
httpx auth flow: attach Bearer, and refresh-once on a 401.
anafpy.auth.callback ¶
Authorization-code capture for the one-time auth login bootstrap.
ANAF redirects the browser (after the certificate step) to the registered callback URL
with ?code=.... The ANAF developer portal rejects http:// callback URLs
(HTTP 400 at registration — verified 2026-07-02), so the registered URL is https://
and there are two ways to capture the code:
- Listener (:class:
CallbackListener/capture_authorization_code): a tiny local server on the callback URL's host/port. Plain HTTP by default (put a TLS terminator in front), or pass anssl.SSLContextto serve TLS directly with a certificate you supply. The listener binds on construction so the browser can be opened only once the port is actually listening — a fast redirect (cached certificate/session) must never outrun the bind. - Paste mode (
parse_redirect_url): run no listener at all. The browser lands on a connection error, but the address bar still holds the full redirect URL; the user pastes it (or just the code) into the CLI. Works everywhere, needs no certificate.
Both capture paths take an expected_state: the CLI binds a random OAuth state
to each login attempt, and a redirect that does not echo it back is rejected (the
listener answers 400 and keeps waiting) — so a forged redirect cannot inject an
attacker's authorization code into the flow (login CSRF).
CallbackListener ¶
CallbackListener(redirect_uri: str, *, ssl_context: SSLContext | None = None, expected_state: str | None = None)
A bound-and-listening receiver for the OAuth redirect.
Binding happens in the constructor (raising :class:AnafConfigError when the
host/port cannot be bound), so callers can open the browser only after the
listener is provably up, then :meth:wait for the redirect. Use as a context
manager (or call :meth:close) to stop the server.
When expected_state is given, only a redirect echoing that OAuth state
completes the wait; anything else (a forged redirect trying to inject a code,
login CSRF) is answered 400 and the listener keeps waiting for the real one.
wait ¶
wait(timeout: float = 180.0) -> str | None
Block until the redirect arrives; the code, or None on timeout.
Raises AnafAuthError on an OAuth error redirect.
parse_redirect_url ¶
parse_redirect_url(pasted: str, *, expected_state: str | None = None) -> str
Extract the authorization code from a pasted redirect URL (paste mode).
Accepts the full redirect URL from the browser address bar, a bare
code=...&... query string, or the code value alone. When
expected_state is given, a pasted URL must echo it back (a bare code —
a deliberate manual extraction — is exempt).
Raises AnafAuthError on an OAuth error redirect, a state mismatch,
or unrecognizable input.
capture_authorization_code ¶
capture_authorization_code(redirect_uri: str, *, timeout: float = 180.0, ssl_context: SSLContext | None = None, expected_state: str | None = None) -> str
Block until ANAF redirects to redirect_uri with a code; return that code.
Bind-and-wait in one call, for callers that already opened the browser (prefer
:class:CallbackListener to bind before opening it). Plain HTTP unless
ssl_context is given, in which case it serves TLS with that context (use
this when the browser must reach the callback over https:// and no
external TLS terminator is in front). expected_state is enforced as in
:class:CallbackListener.
Raises AnafAuthError on an OAuth error redirect or on timeout.