DEV Community

XenonCross2718
XenonCross2718

Posted on

Passwordless Authentication in Postgres — 4 Trade-offs Between Breach Surface and Availability

Passwordless authentication really trades away one breach surface for more concentrated availability dependencies. The least complex defensible outcome is a recovery flow with short-lived, single-use challenges, a small security-event ledger, and no stored message bodies. For a B2B SaaS team, the bill is dominated less by the few hundred bytes in each challenge row than by durable audit events, notification attempts, backups, replicas, and investigation time. If 100,000 accounts average two authentication or recovery events per month, that is 2.4 million event records per year before retries and delivery callbacks. Start there.

TL;DR: Passwordless authentication removes reusable password secrets and the reset path built around them, shrinking credential-stuffing and password-database exposure. It trades that surface for dependence on authenticators, email or SMS delivery, device recovery, and the challenge store. Keep challenge state minimal and ephemeral; retain only the security facts required for abuse investigations and policy. Deleting payloads and old delivery detail reduces forensic depth when an incident appears after the retention window.

What does passwordless authentication really trade away in breach surface?

A password is a shared secret that users reuse, attackers phish, and servers must protect. OWASP recommends breached-password checks, login throttling, and multi-factor authentication because passwords carry those recurring risks. Removing the password removes its verifier database and makes credential stuffing against that endpoint irrelevant. It does not remove account takeover.

The replacement determines where risk moves. A WebAuthn passkey uses public-key credentials scoped to the relying party; the server stores public-key material rather than a reusable authentication secret. A magic link makes the mailbox, delivery path, browser session, and link handling part of the trust boundary. An SMS one-time code inherits telephone-number reassignment, interception, delivery delay, and rate-limit concerns. All are called passwordless, but they do not have equal phishing resistance or failure modes.

Recovery exposes the difference. A passkey-first system that falls back to email can be compromised through that fallback even if its primary ceremony is phishing-resistant. The effective assurance is set by the easiest recovery route, not the strongest button on the sign-in screen.

Fallbacks win.

Passwordless narrows one breach surface while concentrating availability risk in fewer recovery dependencies.

Price the records before choosing the ceremony

An audit-ready design needs enough evidence to answer who requested recovery, which account was targeted, what policy decision occurred, whether a challenge was consumed, and which administrative action followed. It does not need the raw token, OTP, email body, or full authentication assertion. Those values add exposure and rarely improve the routine audit question.

Data class Example Retention approach Failure cost
Live challenge Token digest, expiry, attempt count Minutes, then delete A flow cannot finish after expiry
Security event Account ID, outcome, time, correlation ID Policy-defined and access controlled Less historical evidence after deletion
Delivery telemetry Status, latency bucket, failure class Short window or aggregate Harder diagnosis of old delivery gaps

Suppose each event occupies 1 KiB after indexes and metadata. The earlier baseline is about 2.4 GB before replicas and backups. The exact multiplier depends on database, indexes, replication, and backup policy, so a universal dollar estimate would be fiction. Adding message bodies or verbose request snapshots can increase both storage and breach impact without strengthening the authentication decision.

The material change is to retain compact, immutable outcomes and aggregate old delivery metrics while deleting challenge rows promptly. Keep a documented retention schedule and legal-hold process outside application code. Deliberately stop keeping raw tokens, message content, and indefinite per-attempt network detail. If an abuse report arrives late, the team may know that delivery failed or a challenge was consumed but lack payload-level evidence to reconstruct why. That loss is real, bounded, and explicit.

That is the retention bargain.

Build recovery as a state transition

The database must decide whether a challenge remains valid and consume it atomically. A read followed by a later update lets two workers accept one challenge. Store a keyed digest of the presented secret, never the secret, and return the same public response for known and unknown accounts so the request endpoint does not become an enumeration oracle.

from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import hmac

@dataclass(frozen=True)
class RecoveryResult:
    accepted: bool
    account_id: str | None

def token_digest(server_key: bytes, token: str) -> bytes:
    return hmac.new(server_key, token.encode(), hashlib.sha256).digest()

def consume_recovery(db, server_key: bytes, token: str) -> RecoveryResult:
    digest = token_digest(server_key, token)
    now = datetime.now(timezone.utc)
    # This operation locks or conditionally updates one matching row.
    row = db.consume_unexpired_challenge(digest=digest, consumed_at=now)
    if row is None:
        return RecoveryResult(False, None)
    db.append_security_event(
        account_id=row.account_id,
        event_type="recovery_challenge_consumed",
        occurred_at=now,
        correlation_id=row.correlation_id,
    )
    return RecoveryResult(True, row.account_id)
Enter fullscreen mode Exit fullscreen mode

The transaction boundary should include challenge consumption and the durable security event, or use an outbox in that transaction. Notification dispatch belongs after commit. Otherwise, a transient mail failure can roll back security state, or a successful state change can disappear from the audit trail.

Do not log the submitted token. Avoid putting magic-link tokens where reverse proxies, analytics scripts, referrer headers, or browser history preserve them longer than intended. A landing endpoint can exchange the URL secret for a constrained server-side session and immediately redirect to a clean URL.

Availability is part of the authentication policy

A password can work while email is delayed. A magic link cannot. A passkey can work during a mail outage, but a user who lost every enrolled device still needs recovery. Passwordless availability therefore cannot be summarized by login API uptime.

The dependency moved.

Map the dependency chain: authenticator support, user device access, challenge database, notification queue, delivery channel, and recovery staff or policy. Choose degradation behavior before an outage. Security-sensitive recovery should fail closed when challenge state cannot be verified. The request endpoint may accept work into a durable queue and show a neutral response, but it must not mint an authenticated session because a dependency timed out.

There is friction. Requiring two enrolled passkeys or a passkey plus a separately protected recovery method raises setup effort, yet avoids making one mailbox the sole route into a high-value tenant. For lower-risk accounts, an emailed link with strict expiry, single use, rate controls, and post-recovery notification may be proportionate. Put that decision in a written assurance policy, not only a UI experiment.

Measure request-to-enqueue time, enqueue-to-provider acceptance, confirmed delivery where available, challenge completion, expiry, and retries. Segment failures by tenant, destination domain, or country only where privacy policy permits. A rising completion gap with stable API latency points toward the channel; a spike in expired challenges may indicate delay, abuse, or confusing retries.

Short answer: protect session creation more strongly than message acceptance. A delayed email is frustrating. An unverifiable challenge becoming a session is a security incident.

Test the ugly paths and keep the audit legible

Happy-path unit tests are insufficient. Run concurrent consumption tests against the real database isolation behavior. Exercise duplicate callbacks, delayed jobs arriving after expiry, clock skew, notification retries, tenant suspension, account deletion, and one link opened on two devices. Verify that public responses stay neutral for missing and existing accounts.

Test the override too.

Record policy-relevant transitions rather than prose logs. Event names should be stable, timestamps should be UTC, actor and subject should be distinct, and correlation IDs should join request, dispatch, and consumption without embedding secrets. Restrict and monitor event access; an audit table with account identifiers remains sensitive when tokens are absent.

Deploy schema changes before code that emits new event fields. During rollout, readers should tolerate both versions, and exports should preserve the original event plus its schema version. Do not rewrite history to fit the newest model. Corrections are additional events linked explicitly to the earlier record.

Finish with a tabletop exercise: email unavailable, challenge database read-only, all passkeys lost, and an administrator asked to bypass policy for a senior customer. For each case, name the permitted transition, evidence, user response, and escalation owner. An undocumented manual override means the system is not audit-ready.

Choose passkeys as the primary method when phishing resistance and removal of shared secrets justify enrollment and device-recovery work. Use link or code delivery where the assurance requirement permits dependence on that channel, treating it as authentication infrastructure rather than ordinary messaging. Keep passwords where compatibility or recovery constraints require them, then apply modern password storage, breached-password screening, throttling, and stronger step-up controls as OWASP recommends.

No ceremony eliminates trade-offs. A defensible B2B design states which breach paths were removed, which availability dependencies replaced them, and how recovery preserves the intended assurance. Keep minimal challenge data, compact audit outcomes, and enough telemetry to detect delivery gaps. Delete the rest on schedule, accepting less detail in late investigations.

Further reading

Top comments (0)