keepcalculator

06 Numbers and Powers

OAuth State: Rejecting Reused Callback Values

Card No. 06·51917 · Updated · Standard formula, shown below

OAuth clients can reject reused callback state values by storing each generated state value with a short expiration time and a one-time-use flag, then checking on the callback that the value exists, is unexpired, and has not already been consumed. This prevents an attacker from replaying a stolen or guessed state parameter to complete a login flow they did not initiate.

Background: Why State Exists

The state parameter in OAuth 2.0 and OpenID Connect is a client-generated value included in the authorization request and returned unchanged in the callback. Its primary purpose is to mitigate cross-site request forgery (CSRF): the client verifies that the callback corresponds to a request it actually made, not one an attacker forged. A secondary benefit is carrying a short-lived context, such as the user’s pre-login page, across the redirect to the authorization server and back.

If an attacker can predict or reuse a state value, they can craft a callback URL that passes the client’s validation, potentially linking a victim’s authorization code to the attacker’s session. The OAuth 2.0 specification (RFC 6749) requires the client to generate state values that are “unique” and “unguessable,” but it does not mandate a specific storage or validation scheme. That leaves the details of rejection up to the client implementation.

How Reuse Happens and Why It Is Dangerous

State reuse occurs when the same value is accepted by the client for two different authorization flows. This can happen if the client uses a static value, derives the value from predictable data (like a timestamp), or fails to invalidate the value after first use. An attacker who observes a valid callback URL—for example, in a browser history or a server log—could replay it. If the authorization code is still valid and the client does not check that the state was already consumed, the attacker might exchange the code for tokens.

Even if the authorization code is single-use, reusing state can allow an attacker to mix up sessions. For instance, an attacker could start their own OAuth flow, obtain a valid state, and then trick a victim into visiting a callback URL with that state and the victim’s authorization code. If the client only checks that the state matches a pending request, the attacker’s session would be linked to the victim’s account.

Core Mechanism: Store, Check, Invalidate

The standard defense is to treat each state value as a single-use, expiring credential. The client must:

  • Generate a cryptographically random value (at least 128 bits) for each authorization request.
  • Store it server-side (or in a secure HttpOnly cookie) associated with the user’s session and the specific request.
  • Validate on callback: the value must exist in storage, match the stored session, and not be expired.
  • Consume it immediately after successful validation, so a second callback with the same value is rejected.

This is analogous to a one-time password. The storage can be a database table, an in-memory cache, or a signed cookie. The key is that the value is bound to a single transaction and cannot be used again.

Practical Implementation Steps

Here is a concrete recipe for a typical web application:

  1. Generate state = base64url(randomBytes(32)).
  2. Store it in a server-side session or a database with a timestamp and a flag used=false. Set an expiry of 5–10 minutes.
  3. Redirect the user to the authorization endpoint with that state.
  4. On callback: look up the state in storage. If not found, reject with an error.
  5. Check the expiry: if now > created_at + ttl, reject.
  6. Check the flag: if used=true, reject.
  7. Mark the state as used (or delete it) before processing the authorization code.

If the client uses a cookie to store state, the cookie should be HttpOnly, Secure, and have a short Max-Age. The same validation logic applies to the cookie value.

Common Pitfalls and Misconceptions

A frequent mistake is checking the state after exchanging the code. If the code exchange fails, the state might not be consumed, leaving it open for another attempt. Always validate and consume state first, before any token exchange.

Another misconception is that using a random value is enough. Randomness prevents prediction, but without a one-time-use check, a previously captured state can still be replayed. The storage and invalidation are what make reuse impossible.

Some developers rely on the authorization server’s nonce for CSRF protection in OpenID Connect. While the nonce is similar, it is meant for ID token validation, not for the authorization code flow’s CSRF. The state parameter remains the correct tool for binding the callback to the original request.

Comparing State Handling Approaches

There are several ways to implement state validation, each with trade-offs:

  • Server-side session store: Most secure, as the state is never exposed to the client. Requires server memory or a database.
  • Signed cookie: Stateless and scalable, but the cookie must be tied to the user’s session and still needs a one-time-use mechanism (e.g., storing a hash of the state in a short-lived cache).
  • JWT state: Embed the state as a signed JWT with an expiry. The client can verify the signature and expiry without a store, but cannot easily enforce one-time-use without a revocation list.

For most applications, a simple server-side store is the safest and easiest to implement correctly. The JWT approach is convenient but requires additional logic to prevent replay.

Costs and Performance Considerations

Storing state values adds a small overhead: one write per authorization request and one read/delete on callback. For high-traffic applications, this can be offloaded to a cache like Redis with a short TTL. The cost is negligible compared to the security benefit.

If you use a database, make sure to index the state column and set a cleanup job to delete expired rows. Otherwise, the table can grow indefinitely. A TTL index (e.g., in MongoDB or DynamoDB) can automate this.

Alternatives and Enhancements

Some clients add a PKCE (Proof Key for Code Exchange) challenge, which protects the authorization code from interception. PKCE does not replace state; it addresses a different attack. Using both is recommended for public clients.

Another enhancement is to bind the state to the user’s session ID, so that even if a state is replayed, it cannot be used from a different browser session. This adds a second factor to the validation.

For high-security environments, consider using a short state TTL (e.g., 2 minutes) and rejecting any callback that arrives after the authorization code has expired (usually 10 minutes). This reduces the window for replay.

Frequently Asked Questions

Q: Can I just use a random state and not store it?
A: No. Without storing it, you cannot verify that the callback corresponds to a request you made. An attacker could guess a random value and send a callback with it. You must store the value (or a hash) to validate it.

Q: What if my state is in a cookie? How do I make it one-time-use?
A: Store a hash of the state in a server-side cache with a short TTL. On callback, check that the hash exists and then delete it. This gives you both statelessness and single-use enforcement.

Q: How long should the state be valid?
A> Typically 5–15 minutes, matching the authorization code’s lifetime. If the user takes longer, they may need to restart the login flow. The exact value depends on your user experience.

Q: Is state reuse a real attack?
A: Yes, it is a form of CSRF and session fixation. An attacker can replay a captured callback to force a victim into a session the attacker controls. The OAuth security working group has documented such attacks.

Q: Does PKCE make state unnecessary?
A: No. PKCE protects the authorization code from being used by an attacker who intercepts it. State protects against CSRF. They are complementary and should both be used.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →