aeat.adapters.outbound.aeat.auth._authenticator module

Certificate-backed live-AEAT authenticator.

This module implements the certificate concrete for the application application.auth.AuthProvider contract. It composes CertificateBundle loading, mTLS handshake checks, a CertificateContextProvisioner-backed browser context, and a post-auth login probe into a narrow async provider surface.

The provider returns the imported AeatSession and AeatLoginAssertion records owned by adapters.outbound.aeat.auth._authenticator_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() accepts an optional injectable browser session factory. Unit tests pass an in-process factory that produces a stand-in context honouring the _aeat_certificate_thumbprint marker contract. This lets the whole authenticator exercise run under @pytest.mark.unit without importing Playwright.

  • 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, *, browser_session_factory=None, handshake_verifier=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).

  • TLS handshake verification (via the existing verify_handshake).

  • 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) as auth:
    session = await auth.authenticate()
    assertion = await auth.verify_login(session)

Returned sessions use CertificateSessionDetail, login probes use CertificateLoginAssertionDetail, and persisted resume state is validated against PersistedSessionMetadata. Callers that only need synchronous health, handshake, 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)

verify_handshake(*, url=None)[source]

Run the mTLS smoke probe against url.

Parameters:

url (str | None) – Optional override. When omitted, the authenticator uses Settings.aeat_certificate_verify_url.

Return type:

HandshakeResult

Returns:

A HandshakeResult with the probe outcome.

async authenticate(*, browser_session=None, target_url=None)[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 handshake plus browser login flow. Fresh contexts are created through CertificateContextProvisioner so the AEAT origin receives the configured client certificate.

Parameters:
  • browser_session (BrowserSessionLike | None) – Optional existing browser session to reuse.

  • target_url (str | None) – Optional override URL for the authentication target.

Return type:

AeatSession

Returns:

An authenticated AeatSession ready for downstream use.

Raises:
  • AeatLoginAssertionError – When the browser session factory returns a context missing the thumbprint marker, or when the login probe 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 handshake fails, or verify_login still returns certificate_recognised=False — 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_login(session, *, target_url=None)[source]

Navigate the authenticated context to target_url.

The assertion record captures three independent signals:

  • handshake_success — the TLS handshake attached to the session completed successfully.

  • certificate_recognised — the post-auth navigation returned a non-challenge HTTP response.

  • parsed_nif — the NIF / NIE extracted from the certificate subject (always populated when the session carries a cert; None only in exceptional structural failures).

Parameters:
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:
async verify(session, *, target_url=None)[source]

Provider-protocol alias for verify_login().

Return type:

AeatLoginAssertion

Returns:

A AeatLoginAssertion describing the verification outcome.

Parameters:
async capture_storage_state(session)[source]

Persist the active Playwright state and PersistedSessionMetadata.

Return type:

Path

Parameters:

session (AeatSession)

async resume_from_storage_state(path, *, browser_session=None, target_url=None)[source]

Resume a certificate AeatSession from encrypted storage.

The persisted browser state and PersistedSessionMetadata are validated before a CertificateContextProvisioner opens a context with the restored storage state. A successful live probe refreshes authenticated_at and idle_deadline before the session is returned.

Return type:

AeatSession

Parameters:
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 setconfigured=False, severity info, summary application.auth.certificate.health.path_unset. An undeclared state, not a fault.

  • path set, file missingconfigured=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_login() call to finish its navigation before tearing down the browser context, so a page cannot be closed out from under a running probe. A one-way _closing latch is set under the lock before the drain wait so that a new verify_login cannot slip in between the wait returning and the teardown acquiring the lock — the latch forces any arriving probe to raise.

After close() returns, the authenticator is re-usable (the latch is reset, the browser session is nulled, the context is nulled). reauthenticate() depends on this re-use path.

Return type:

None