Guide 124 worked guide

How to assess replay protection on one-time actions

Assess replay protection on one-time actions by separating durable consumption from response caching and checking resend semantics, retries, and concurrent requests.

For a one-time action, check two separate properties: the same authority cannot cause the effect twice, and a legitimate retry after a lost response does not create a second effect. Identify what makes the action unique, what identity and resource it is bound to, when it expires, and which durable state change consumes it. Then follow duplicate, concurrent, expired, revoked and cross-account requests to the actual side effect. A token's timestamp or random appearance alone does not prove single use; the server must enforce it. If the final worker or transaction boundary is unknown, record an investigation rather than claiming replay is possible or impossible. OWASP's Transaction Authorization Cheat Sheet calls for operation-specific authorization credentials, a limited validity period and a final check before execution.

Know which “one time” promise you are reviewing

The phrase can refer to different mechanisms. A sign-in one-time password proves possession during authentication; a password-reset link authorizes a credential change; an invitation token may authorize account membership; a payment idempotency key identifies a client retry. Each has a different subject, effect and reuse rule. NIST's current SP 800-63B says a verifier accepts a given OTP only once while valid. That is an authentication rule; it does not by itself prove a separate business action can happen only once. OWASP's Forgot Password Cheat Sheet similarly calls for reset tokens that are bound to a user, single-use and expiring. Apply the corresponding principle to the particular action, without treating an invitation as an OTP standard claim.

HTTP method names do not settle this either. RFC 9110 describes the semantics of idempotent methods, where repeating an identical request has the same intended effect as one request. A POST can be made safe to retry through an application protocol, but a successful retry response is different from authorizing a second grant. Review the persisted effect, not just the status code.

Work an illustrative invitation case

Illustrative application and tokens only; no invitation was used. Alder Workspace lets an account administrator invite a named colleague as a repository viewer. The invitation has an opaque random token, a target account, invited identity, fixed role, expiry and a pending state. Accepting it creates membership and marks the invitation accepted. The intended rule is: only the invited identity can accept while the administrator's invitation remains valid; one invitation can create at most one membership; a retry may return the first result without creating another grant.

The reviewer sketches the state transition:

State and input Server decision Durable effect
Pending, valid token, correct identity and account Accept once Mark accepted and create the intended membership
Accepted, exact retry after response loss Return known outcome or harmless “already used” No new membership or role change
Pending, wrong identity or account Deny No effect; do not disclose private account data
Expired or administratively revoked Deny No effect
Two simultaneous valid accepts One wins the durable transition One membership effect, never two

This is the review artifact. The exact response to an exact retry is a product decision. The security invariant is that it causes no additional or altered grant. The server should derive account, identity and role from trusted invitation state and current policy, not from client-submitted versions of those fields. Expiry limits time, but without a consumed state it still allows multiple uses within the validity window. A used=true field written only after creating membership may leave a race or crash gap.

Trace the token to the effect

1. Define the action's identity. Is “one” per token, invited identity, target account, payment attempt, order or transaction? If an admin can issue two separate invitations, two tokens might be valid by design. Ask the product owner whether accepting one should cancel the other and what happens if the target role changes before acceptance. Do not infer policy from a database unique key alone.

2. Inspect issuance. Find the token generator, storage, expiry and binding to account, intended identity, action and role. The token should be unguessable, but randomness only makes forgery harder; it does not prevent reuse by someone who already has it. Check whether resend creates a new token and invalidates the old one, and whether administrative revocation reaches the acceptance path promptly. Avoid printing real tokens into a report or log.

3. Inspect the final accept decision. Follow the request through login, account selection, invitation lookup, current policy and membership creation. Does the service check the invitation's stored identity rather than a submitted email? Does it confirm the account is active and the role remains grantable? Is the read of pending and transition to accepted coupled atomically to the membership effect or a durable intent that cannot duplicate? OWASP's Business Logic Security Cheat Sheet identifies check-then-act races and recommends persistent completion state for one-time operations.

4. Follow asynchronous work. Some systems accept the token synchronously but queue an email, membership write or external action. Ask whether the worker verifies the same bound intent and uses a stable effect ID. A queue's at-least-once delivery is not a vulnerability by itself if the consumer is idempotent. A successful HTTP response before a durable effect is recorded may create an ambiguous retry path that the owner must resolve.

5. Review all replay dimensions in a safe environment. With synthetic invitations and permission from the service owner, compare first use, exact retry, simultaneous acceptance, expired use, revoked use and use by a different invited identity. Observe both the response and durable membership/role state. Do not submit real customer invitations, replay production tokens or generate uncontrolled parallel traffic. If staging does not share production transaction semantics, label that limit.

Follow a lost response and a changed retry

Use a concrete record sequence. These are synthetic records and expected outcomes, not executed requests. Invitation I7 binds account Cedar, invited identity M9 and role viewer; client input is never authoritative for those terms.

Moment Invitation / operation Membership Result
Before accept I7 pending, terms (Cedar,M9,viewer), version 3 None No grant yet
First accept commits I7 accepted; operation ACCEPT-I7 records fixed terms and membership M17 M17 is viewer Response is lost after commit
Exact retry Same operation and terms; current caller may read its outcome M17 remains viewer Return completed outcome, no new grant
Retry requests role=admin Same I7, different material terms M17 remains viewer Reject changed request; do not reinterpret spent authority
Separate invitation I8 was revoked before acceptance I8 revoked, current version 5 Unchanged Deny even if token has not reached its expiry time

A uniqueness-only implementation can still be wrong:

upsert membership(account, identity, role=request.role)
mark invitation accepted

The unique (account, identity) row prevents duplicate memberships. It does not prevent the retry from overwriting viewer with admin. The durable fact the business cares about is the grant's terms, not the number of rows.

The proposed operation reads the stored invitation terms, checks its current validity, and atomically consumes the invitation with one membership effect or durable intent. An accepted operation is looked up by I7, but the caller must still be authorized to see that result. A changed role is not an exact retry. A new role requires a separate, currently authorized membership-change action.

If membership creation and consumption live in different stores, a single transaction may not cover them. The design then needs a recoverable intent and a consumer that recognizes the same immutable effect identity. Do not claim this table proves cross-service atomicity. The implementation review must show what happens after consumption but before membership creation, and after creation but before acknowledgement.

The filled verdict has three branches: reject the duplicate-row hypothesis when the unique effect is mandatory; retain the changed-role concern if the upsert uses request terms; reject acceptance of I8 only when the final decision reads its current revoked state. These are separate conclusions. A short expiry answers none of the retry cases by itself.

Distinguish a replay from a legitimate retry

A caller may lose the response after the server committed a one-time action. Forcing them to repeat with a new token can be confusing or unsafe. A common design records an operation identifier and the committed outcome so an exact retry receives the same or an “already completed” answer with no new effect. It should reject reuse of the same identifier with different material parameters. This is an application design pattern, not a claim that all one-time tokens should remain usable after consumption. The invitation authority is spent; the result can still be looked up under current access rules. Define retention for spent-token/effect records against the retry and replay horizon. Expiring a convenience response cache must not turn an old invitation into pending authority; the durable membership operation or consumed state still prevents recreation.

If a request succeeds on the first attempt but the client sees a timeout, examine whether retry semantics are documented, whether a new token could trigger a second action, and whether the client can query final status without revealing protected membership details. Do not call a server-side 500 proof of nonexecution. The durable record and downstream effects decide whether anything happened.

Decide what a resend changes

Alder's invitation resend can either deliver the same still-valid authority again or replace it with a newly issued token under a defined policy. The reviewer should establish which behavior the application implements. If replacement invalidates the old token, that invalidation must reach the final acceptance decision. If both remain valid, they should still resolve to the same intended membership effect and fixed terms.

A resend should not silently extend a revoked invitation or upgrade its role. Those are separate authority changes. The operator may legitimately create a new invitation, but it needs current grant permission and a clear relationship to the old record. This prevents a delivery convenience from turning spent or revoked authority back into pending access.

Keep outcome lookup separate from token redemption

After I7 commits, the client may need to discover whether membership M17 exists because the response was lost. Design that lookup around the current authenticated recipient and the recorded operation, with the appropriate privacy boundary. It does not need to restore the invitation to a redeemable state.

If the result cache has expired, the durable consumed state must still prevent a second grant. The service can return a bounded completed or unavailable-result status according to its contract. It must not treat “cached response absent” as “action never happened.” The membership and operation records, plus any downstream effect identity, provide the evidence needed for reconciliation.

These distinctions make the replay review useful during ordinary failures. Users can retry or seek status without receiving fresh authority, while the service preserves the original account, recipient, and role even across concurrent requests or a crash after commit.

For a confirmed gap, request a fix tied to the invariant: an atomically consumed, action-bound invitation and an idempotent membership effect. At a new pinned revision, the reviewer needs positive evidence that the normal acceptance works and every duplicate path leaves the grant unchanged. A missing alert, a short expiry or a frontend button that disables itself is not closure evidence. If the case concerns a checkout ledger rather than invitations, continue with the credit-flow review. For role and object checks at acceptance, use API authorization beyond login.