cadrumo.adapters.outbound.aeat.auth.authenticator module

Certificate-backed live-AEAT authenticator.

This module implements the certificate concrete for the application application.auth.providers.AuthProvider contract. It composes CertificateBundle loading, a CertificateContextProvisioner-backed browser context, and the canonical protected-resource probe into a narrow async provider surface.

The provider returns the imported AeatSession and AeatLoginAssertion records owned by application.auth.session_types. Captured Playwright storage state is written through the encrypted session store with PersistedSessionMetadata, then resumed only after hash, idle-deadline, certificate thumbprint, certificate subject, and live probe checks pass.

Design notes:

  • The module holds an 18-minute session idle TTL as a code-level constant. The value is deliberately not an env var — the operator surface is kept narrow, and AEAT’s observed idle window is ~20 minutes (the extra 2 minutes is safety margin).

  • authenticate() uses the configured browser-session factory. The authenticator owns and deterministically closes every session it creates.

  • reauthenticate() is single-shot. Callers cap retries at ONE per downstream call-site; a second consecutive failure raises AeatSessionExpiredError upwards rather than loop.

AEAT_SESSION_IDLE_TTL: Final[timedelta]

Maximum idle lifetime for an authenticated AEAT Playwright session.

AEAT’s observed server-side idle window is ~20 minutes; 18 minutes leaves a 2-minute safety margin before the next downstream call would see a 401/403. Tuning this value is a code change, not an env-var change — the operator surface stays narrow.

AEAT_LOGIN_NAVIGATION_TIMEOUT_MS: Final[int]

Playwright navigation timeout for post-auth verification probes.

class AeatAuthenticator(settings, *, credentials, browser_session_factory=None, navigation_timeout_ms=30000, certificate_health_check=None)[source]

Bases: object

Certificate implementation of the application AuthProvider.

The authenticator owns:

  • Certificate loading and health evaluation (via the existing module-level load_certificate / health surface).

  • Playwright browser-context construction with the cert wired through (via an injectable browser session factory).

  • Login-assertion verification.

  • Session lifecycle: authenticate, reauthenticate, close.

Use as an async context manager:

async with AeatAuthenticator(settings, credentials=credentials) as auth:
    session = await auth.authenticate()
    assertion = await auth.verify(session)

Returned sessions use CertificateSessionDetail, login probes use CertificateLoginAssertionDetail, and persisted resume state is validated against PersistedSessionMetadata. Callers that only need synchronous health or NIF extraction can instantiate without entering the async context.

Parameters:
kind: AuthProviderKind
load_certificate()[source]

Load the configured PKCS#12 bundle and return a LoadedCertificate.

Return type:

LoadedCertificate

health(*, now=None)[source]

Return a CertificateHealth for the configured bundle.

Return type:

CertificateHealth

Parameters:

now (datetime | None)

async authenticate()[source]

Produce an authenticated AeatSession.

The method first attempts to resume a previously captured Playwright storage_state backed by PersistedSessionMetadata. If that persisted state is missing, malformed, stale, certificate-mismatched, or fails a live verification probe, it is deleted and the method falls back to a fresh certificate-backed browser login flow. Fresh contexts are created through CertificateContextProvisioner so the AEAT origin receives the configured client certificate.

Return type:

AeatSession

Returns:

An authenticated AeatSession ready for downstream use.

Raises:
  • AeatLoginAssertionError – When the canonical protected-resource assertion does not prove a successful, exact final resource.

  • AuthConfigurationError – When async authentication has no browser session factory or certificate credentials are incomplete.

  • BrowserError – When browser launch or context construction fails.

  • Exception – Re-raised when storage-state capture fails after a successful context creation.

async reauthenticate(session)[source]

Drop the current context and re-run authenticate().

Single-shot. The method itself does not retry; callers cap retries at one per downstream call-site. A second consecutive failure — whether the cert load fails, the protected-resource probe fails — MUST raise AeatSessionExpiredError upwards rather than loop.

Not atomic across the teardown + authenticate boundary. If another task calls authenticate() between this method’s close() completing and its authenticate() starting, the second call wins the “already has active session” guard check and this call raises AeatLoginAssertionError. External serialisation is required if concurrent reauthenticate / authenticate is a real scenario for the caller.

Parameters:

session (AeatSession) – The session to replace. Passed for traceability (logging, audit) and to document that the caller acknowledges it is discarded.

Return type:

AeatSession

Returns:

A fresh AeatSession with a new authenticated_at + idle_deadline.

async verify(session)[source]

Navigate the authenticated context to the canonical protected resource.

The assertion is valid only when Playwright reports a successful response and the final scheme, host, and path remain pinned to the accepted certificate resource.

Parameters:

session (AeatSession) – The AeatSession returned from authenticate().

Return type:

AeatLoginAssertion

Returns:

A frozen AeatLoginAssertion. Negative results (is_valid=False) are returned as records, not raised — callers may invoke reauthenticate() once and re-verify.

Raises:
describe()[source]

Return an AuthProviderDescription with a safe summary of the configured provider.

Three distinct certificate states surface here, each with its own health_summary and health_severity so a downstream consumer can render them differently and so the loudest severity is reserved for genuine faults (round-5 B1 + minor):

  • no path set — configured=False, severity info, summary application.auth.certificate.health.path_unset. An undeclared state, not a fault.

  • path set, file missing — configured=False, severity warning, summary application.auth.certificate.health.file_missing. The operator persisted a path that no longer resolves; the slot is operationally unusable until the file returns or a new path is supplied.

  • path set, file present — proceeds into the password + load + health-check chain below; configured becomes True and severity reflects the certificate’s expiry health.

Return type:

AuthProviderDescription

async close()[source]

Release the browser context + session. Idempotent.

Waits for any in-flight verify() call to finish its navigation before tearing down the browser context, so a page cannot be closed out from under a running probe. The lifecycle barrier serializes the complete close sequence. Each caller registers a counted close intent before waiting for that gate, so new work remains barred until every registered closer has finished.

After every registered close caller returns, the authenticator is re-usable (the browser session and context are nulled). reauthenticate() depends on this re-use path.

Return type:

None