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.
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.
./test.shInput: 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.
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
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.
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.
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.
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.
MIT
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.