Rightshold™ Sign up

Answers · web security

Why are my one-time links already used when people open them?

Short answer: Something opened the link before your user did. Chat apps fetch links to build previews (Slack's Slackbot-LinkExpanding, for example), and email security scanners such as Safe Links in Microsoft Defender for Office 365 check links in incoming mail, so a link that is consumed on the first GET request is used up by a bot. Supabase's docs describe this for magic links, where prefetching makes the link show as expired. The fix is to stop letting a GET request consume anything: the link opens a page with a confirm button, and only the button's POST spends the token. Ignoring HEAD requests and known bot user agents helps, but treat it as a filter, not the fix, because scanners don't all identify themselves. If the link carries a secret for the page itself, as in a share link to encrypted content, put it in the URL fragment (after #), which browsers don't send to the server, and keep it from leaking from the page: Referrer-Policy: no-referrer, a strict Content-Security-Policy and no third-party scripts.

Checklist

  1. Make GET safe: opening the link shows a page that says what will happen, with a button. Clicking it sends a POST that checks and consumes the token. HTTP defines GET and HEAD as safe methods, and automated clients rely on that.
  2. Don't consume on HEAD or OPTIONS, and don't consume on GET from known preview user agents (Slackbot-LinkExpanding, Twitterbot, facebookexternalhit, Discordbot and the like). Expect scanners with browser-like user agents too, which is why the button matters.
  3. Make the token single-use on the server and short-lived, and bind the session it creates to the browser that pressed the button, not to whoever fetched the URL first.
  4. Give people a way back: if a link was already used or expired, show a clear page with a button to send a new one, not a dead end. For sign-in, offer a one-time code as an alternative to the link.
  5. For secrets meant for the page itself (decryption keys for shared content), use the fragment: https://example.com/s/abc#key. The fragment stays in the browser; read it with JavaScript after the user confirms.
  6. Set Referrer-Policy: no-referrer on these pages, so a token in the path or query is never sent to other sites in the Referer header (browsers never include the fragment there).
  7. Serve a strict Content-Security-Policy and no third-party scripts (analytics, chat widgets, tag managers) on the page: any script running there can read location.hash and the page's contents.
  8. Keep tokens out of logs and analytics: strip them from access logs and error reports, and don't log full URLs on these routes.

Who fetches your links

Why the fragment and the headers matter

Ask Rightshold (free, no key)

Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “This request hit my one-time link. Does it carry a valid Web Bot Auth signature from a known agent, and does my policy allow it?”

One-click setup

{
  "tool": "gate_check",
  "arguments": {
    "policy": {
      "require_web_bot_auth": true
    },
    "request": {
      "method": "GET",
      "url": "<the requested URL>",
      "headers": {
        "signature-agent": "<as received>",
        "signature-input": "<as received>",
        "signature": "<as received>"
      }
    }
  }
}

Rightshold checks agent signatures and identities you pass it. It does not sit in front of your links or block bots, and it cannot tell you who fetched a link that carried no signature.

Sources

Related

Not legal advice. Updated 2026-10-29. JSON · Markdown · All answers

Get a free API key in one call

No card and no email needed. 100 free credits; the license and policy checks stay free without a key.

curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_...   # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
  -H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'