Skip to content
SHEET ARCHITECTURE · CASE STUDY

Fail-Closed by Construction: OAuth 2.1 and a SAML Replay Guard in Postgres

Choosing the store whose failure mode a security control can actually survive, then hardening it through two review passes that each found a real bug

ARCHITECTURE SR. SOFTWARE ENGINEERSTARTUP
494/494
AUTH SUITE GREEN AT MERGE

The Problem

The monolith needed a real OAuth 2.1 authorization server, largely to support SAML sign-in for tenants running their own identity providers under SOC 2 expectations. The existing session and integration auth in that codebase had grown up around a Redis-backed cache: check a key, trust the result, move on. It worked, but it had a property nobody had stress-tested on purpose. If Redis was unreachable, the checks it backed did not block the request; they let it through. That is a defensible default for a session cache. It is not a defensible default for a control whose entire job is to say no.

SAML sign-in carries that risk directly. A captured assertion is a bearer credential until it expires, and if it gets replayed, whether by an attacker, a buggy retry, or a curious proxy, the identity provider has no way to know the second presentation was not the user logging in again. The system accepting the assertion has to refuse the second presentation. I designed the authorization server core, RS256 signing behind a JWKS endpoint, an encrypted signing key at rest, SOC 2 audit logging, and I owned the replay defense specifically: it was the one control that could not inherit the codebase’s existing failure mode and still do its job.

The Approach

The reflexive answer was Redis. It was already in the stack, already fast, already where every other “have we seen this before” check in this codebase lived. I rejected it for exactly that reason: the codebase’s Redis-backed auth controls fail open, and a replay guard that fails open is not a guard, it is a suggestion. I put the guard in Postgres instead, behind a single INSERT ... ON CONFLICT DO NOTHING against a table keyed on the assertion identifier per tenant. The insert either lands, meaning this assertion has never been seen and the request proceeds, or it conflicts, meaning it has, and the request is rejected. If Postgres itself is unavailable, the insert fails outright and the login fails with it. There is no code path where “the store did not answer” resolves to “let the user in.”

FIG. 14 / OAUTH / SAML REPLAY GUARD · ONE-TIME-USE ASSERTION CHECK IN POSTGRES
MERGED · 494/494 · PEN TEST PENDING · SHEET SEC-01
ASSERTION VALID SESSION HASH + INSERT CONFLICT = REJECT NEW ROW = ACCEPT AUTH SERVICE REPLAY DETECTOR ATOMIC DB GUARD REPLAY GUARD POSTGRES UNIQUE INDEX SHA-256 HASHES · PER TENANT SAML IdP PER-TENANT IdP IDENTITY PROVIDER PROTECTED APP PLATFORM SERVICES AUTHENTICATED TENANT

Only the hash goes in the logs. The replay table and every audit line that references an assertion store a SHA-256 hash of the assertion identifier, never the raw ID. A raw identifier is a real value an attacker could probe the system with; a hash gives operators everything they need to spot a duplicate without ever writing the credential itself to disk.

The suite closed at 494 of 494 at merge, but what matters more is what two review passes caught before the suite ran green for the last time. A teammate’s review on the assertion-handling path caught a confused-deputy bug: a validation path that could be tricked into trusting configuration belonging to a different tenant than the one presenting the assertion, the kind of defect that looks correct in isolation and only misbehaves under a specific cross-tenant input. I fixed it before merge. Separately, I ran an adversarial security pass against my own one-time-use mechanism, on the theory that whoever designs a control is the worst person to find its own holes. That pass found a real clock-drift bug: the window bounding “one time” was sensitive to a small amount of skew between the issuing and verifying paths, which would have let a replay through in exactly the narrow case the guard exists to stop. I fixed it thirty-four minutes later, same session, before it ever reached a reviewer. Both catches are worth naming, not because a broken system shipped anyway, but because they are what review looks like when it works: a real defect, found by a real pass, closed before anyone downstream had to find it the hard way.

Results

The authorization server and the replay guard merged into the monolith with the full auth suite at 494 of 494. I do not have a verified production traffic date for this work, so I will not give the SAML path a shipped-and-serving-users close it has not earned yet. What I can say plainly is that it merged fail-closed by construction, with two independent passes each finding one real bug before either could become someone else’s incident. The work is only weeks old and has not yet faced outside adversaries; an outside penetration test is the next gate it has to clear.

The same platform needed a second, unrelated auth surface: a standalone JWT issuer for a separate document pipeline, decoupled from the monolith on purpose. That service applies the same instinct to a different shape of risk. Brute-force lockout on login, a decoy-hash comparison so a failed lookup and a failed password check take the same code path and time, and refresh-token rotation with family revocation so a stolen refresh token buys an attacker exactly one use before the whole lineage gets burned. It is a smaller surface solving a related problem the same way: assume the attacker gets one shot, and design so that one shot is all they get. The pipeline auth service covers that build in full.

Pick the dependency whose failure mode you can live with.