DET-BETA-006 — join token sharing
Rule ID: DET-BETA-006
Title: Beta join token claimed from IP inconsistent with prior state-check history — token sharing
Category: beta
Last validated: 2026-06-18 (beta-phase2 catalog; grounded against as-built beta_join.py)
State: live — FLAG_BETA_PHASE2_ACCESS is ON in prod; POST /api/beta/join/<token>/claim emits beta.join.claimed audit events to audit_log. Claim events are queryable from Raptor Postgres directly (no drain dependency).
Why this detection exists
A beta invite token is minted for a specific tester email and is single-use. If the tester shares their invite link with a third party, the third party can open the state-check page, proceed through the NDA flow, and then claim the token via POST /api/beta/join/<token>/claim. The account is created with the tester's email address (the one embedded in the token), so the tester cannot then use their own invite — they are left without an account.
The detection is informational: the account created is legitimate (tester's email, valid single-use token). The structural harm is that the tester loses access to their own invite. The detection surfaces the pattern so the operator can issue a replacement token if warranted.
Two signals are observable from the as-built claim endpoint (backend_v2/api/routes/beta_join.py):
-
IP mismatch between state-check and claim. The
join_claim()handler writesbeta.join.claimedtoaudit_logwithip_prefix(computed as /24 for IPv4, /48 for IPv6, via_ip_prefix()). If prior state-check requests for the samejtiwere observed from a different IP /24 (available once the Heroku drain is wired), the claim IP mismatch is the signal. -
Rapid re-claim attempt after consumption. If the original tester then tries to use their own link after the third party consumed it,
call_consume(token, check_only=False)returnsstatus == "already_consumed". Thejoin_claim()handler logs this asbeta_join.claim already_consumed email_hash=... jti=...at WARNING level and returns409 {"error": "already_claimed"}. A 409 on ajtithat was successfully claimed from a different IP /24 within 60 minutes is the sharing-confirmation signal.
Telemetry source
audit_logtable in Raptor Postgres:beta.join.claimedevent rows includecontextJSON withtester_email_hash,jti, andip_prefix;target_idcolumn stores thejti. Queryable directly.- Raptor app logs:
beta_join.claim already_consumed email_hash=... jti=...WARNING log for every 409 response from the/claimendpoint. audit_logtable:beta.join.geoblock_rejectedevent rows include the samejtiandip_prefix— cross-join with claimed rows to detect geo-evasion followed by successful claim from a different IP.
Telemetry availability: audit_log is Raptor Postgres, queryable directly (no Heroku drain dependency). This detection is operationally stronger than DET-BETA-005 and DET-BETA-007 for the 409-based Rule 2 signal.
Statistical method + baseline window
This is an event-pair detection, not a statistical threshold. The signal is structural:
- Rule 1 (IP mismatch): For a given
jti, if theip_prefixin thebeta.join.claimedaudit event differs from the IP /24 observed in prior state-check access logs (when the drain is wired), log as LOW-to-MEDIUM depending on prefix distance. Same /16 = FP territory. Different country ASN = HIGH territory. - Rule 2 (rapid re-claim attempt): A
beta_join.claim already_consumedlog line onjti=Xwithin 60 minutes of a successfulbeta.join.claimedevent on the samejti, and the subsequent attempt is from a different IP /24 than the original claim.
Pre-baseline: no rolling window needed — these are point-in-time event pairs with a 60-minute observation window.
Threshold + expected FP rate
- HIGH trigger:
beta.join.claimedIP /24 + country differs from any prior state-check IP /24 for the samejti(requires drain), AND the difference is cross-country (e.g., state-check from US, claim from EU). - MEDIUM trigger:
beta.join.claimed+ rapid 409 on samejtifrom different IP /24 within 60 min. - LOW trigger:
beta.join.claimed+ rapid 409 on samejtifrom same IP /24 within 60 min (suggests tester retried their own link after sharing; sharer used the same network). - Expected FP rate: moderate for Rule 1 (IP mismatch alone). Legitimate FPs:
- Tester using a VPN or changing network between the state check (e.g., on mobile data) and the claim (e.g., on WiFi).
- Tester opening the link on a mobile device after checking it on desktop.
- Tester on a corporate NAT that maps to different external IPs across requests.
Rule 2 (rapid 409 from different IP) is the higher-confidence signal. Rule 1 alone is informational, not actionable.
Alert route
- HIGH (cross-country IP shift on same
jtibetween state-check and claim):#raxx-ops-alert-sev2-5(ET hours, 13:00–20:00 UTC) /#raxx-ops-alert-sev2(off-hours). Per-event. - MEDIUM (rapid 409 on same
jtifrom different IP /24 within 60 min): ops@ daily digest. Operator decides whether to issue a replacement token. - LOW (IP mismatch within same /16, or rapid 409 from same IP /24):
docs/detections/_log/silent entry for baseline tracking.
Escalation owner
- operator — this is a tester-support concern first. If the original tester is left without an account, operator issues a replacement token via the Console beta tester admin action (OQ-4 path). No automated account action.
- security-agent — only if the IP mismatch pattern includes geo-blocked countries (suggests the tester shared their link with someone in EU/QC to bypass the geo-block). Cross-reference with DET-BETA-007 in that case.
Test fixture / synthetic positive
See _fixtures/join_token_sharing_positive.json — synthetic pair: (1) beta.join.claimed event for jti=synth-jti-001 from IP prefix 192.0.2.0/24, (2) beta_join.claim already_consumed log for same jti from IP prefix 198.51.100.0/24 43 minutes later, representing the original tester attempting to use their own link after a third party claimed it.
Manual query (Postgres direct — no drain required for Rule 2)
-- Find claimed events; cross-reference logs for rapid re-claim attempts on the same jti
SELECT target_id AS jti,
context->>'tester_email_hash' AS email_hash,
ip_prefix,
created_at
FROM audit_log
WHERE action = 'beta.join.claimed'
ORDER BY created_at DESC
LIMIT 100;
Cross-reference returned jti values against Raptor logs for beta_join.claim already_consumed entries on the same jti within 60 minutes:
heroku logs --app raxx-api-prod --num 5000 \
| grep 'beta_join.claim already_consumed'
What NOT to do
- Do not deactivate the account created by the third-party claimer from this detection alone. The account is legitimate (tester's email, valid token). Deactivation is an operator decision.
- Do not automatically reissue a replacement token. Confirm with the original tester out-of-band that they did not intend to share the link.
- Do not conflate with DET-BETA-005 (enumeration). DET-BETA-006 fires on valid tokens that were successfully claimed from an inconsistent IP; DET-BETA-005 fires on high-volume invalid-token probes from one IP.