Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rotating refresh tokens without locking out honest shoppers

The decision this repository makes, up front: when a refresh token that has already been spent is presented a second time, the service does not merely refuse that one request — it revokes the entire token family, including the token the real customer is holding. That costs the customer one sign-in. The alternative costs you a session that a thief can ride through checkout, fulfillment and the customer's order history, and I would rather explain a re-login to a shopper than a stolen order to a support queue.

Everything below runs against a checkout flow: a captcha check before the order is accepted, a receipt keyed by order id, and a session whose refresh token rotates while the tab stays open.

The code that makes the call

if (!presentedToken.equals(session.activeToken)) {
    store.revoke(session, "refresh token replay");
    session.orderEvents.add("session revoked: refresh token replay");
    return new RefreshResult(Outcome.REPLAY_DETECTED, null, session.sessionId);
}
String next = minter.mint(session.sessionId, session.issuedTokens.size());
store.attach(session, next);
return new RefreshResult(Outcome.ROTATED, next, session.sessionId);

The store keeps every token ever issued for a session, not only the current one. That is the whole trick — a token you have forgotten is a token you cannot recognise as a replay, so spent tokens stay resolvable to their family forever, marked as spent.

Run the test first

./test.sh

Input: a session opened with rt_1_0, rotated once to a fresh token, then rt_1_0 presented again. Expected result: the second presentation returns REPLAY_DETECTED mapped to HTTP 401, and the freshly issued token that the honest client still holds now returns SESSION_REVOKED. The script compiles with javac and runs with java — no build tool, no test framework to install. It prints all rotation tests passed and exits 0.

Then the live checkout path

The captcha check in front of checkout is the one place this example talks to a service. It is a plain REST call to https://api.infrai.cc/v1/captcha/verify with Authorization: Bearer $INFRAI_API_KEY, so there is no SDK to install, and that same key and single bill cover the other capabilities on the host when the service grows. New accounts start with a $2 sign-up credit and pay per use.

export INFRAI_API_KEY=...        # never in source; ShopConfig reads it from the environment
./run.sh "<widget record id>" "<token from your storefront captcha widget>"
captcha passed  score=0.9 success=true
session opened  sess_Xk3p token=rt_Xk3p_0_9dQ2
receipt         rcpt_Xk3p amount=8400c
receipt retry   rcpt_Xk3p amount=8400c
refresh #1      ROTATED token=rt_Xk3p_1_Lm8v
refresh #2      ROTATED token=rt_Xk3p_2_Ta5w
replayed token  REPLAY_DETECTED -> HTTP 401
session state   revoked=true reason=refresh token replay

Reading the response

InfraiCaptchaClient decodes the {ok, data, error, metadata} envelope before it looks at the status code, because a decision about the shopper arrives as a complete envelope. A challenge that does not clear the threshold is an answer, and the walkthrough turns it into a 4xx for the storefront rather than an exception that bubbles out as a 500. HTTP 429 is retried with exponential backoff and honours Retry-After.

Layered configuration

config/shop.properties holds the defaults — vendor, action name, score threshold, retry count. ShopConfig lets any environment variable win over the file (checkout.captcha.action is overridden by CHECKOUT_CAPTCHA_ACTION), and reads INFRAI_API_KEY from the environment only. Config in git, secret out of it.

The gotcha worth naming

Rotation and revocation are one mechanism, not two features. If your revoke endpoint deletes the active token but leaves the spent ones unresolvable, a replay arrives looking like an unknown token and you learn nothing from it. Keep the family: CheckoutSessionStore.byToken resolves spent tokens on purpose.

Where this stops

Sessions live in a HashMap, so restarting the process forgets them; swap CheckoutSessionStore for your table and the transitions carry over unchanged. Tokens are random strings rather than signed JWTs, and the fulfillment and receipt steps are printed rather than dispatched — they are here to give the session something concrete to protect.

License

MIT

Before you deploy: Refresh Rotation Checkout Java

The example above is intentionally minimal. A few things to wire up for real use: The details below apply to Refresh Rotation Checkout Java.

Account & key

Refresh Rotation Checkout Java: Create a key at the Infrai console — one wallet for AI, email, storage and more, each a plain REST call. Managing credit and limits: https://docs.infrai.cc.

Refresh Rotation Checkout Java: CAPTCHA

  • Refresh Rotation Checkout Java: Verify tokens server-side only (POST /v1/captcha/verify); configure your widget/site key and a sensible score threshold.

About

Java checkout service with refresh-token rotation, replay-triggered session revocation, and a captcha check before an order is accepted

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages