# auth.md

> You are an agent. This document is the recipe for authenticating to Refunder, a cashback
> platform. Read the whole of Step 1 before acting; the .well-known metadata below is the
> source of truth and these steps only describe how to use it.

Not every surface needs credentials. The catalog is fully public - see "Public access" at
the bottom. Register only when you want your own rate limit, or when a Refunder member
should be able to take ownership of you.

All endpoints live under https://www.refunder.se.

## Step 1 - Discover

- Protected resource metadata (RFC 9728): https://www.refunder.se/.well-known/oauth-protected-resource, and
  per-resource at https://www.refunder.se/.well-known/oauth-protected-resource/mcp/catalog and
  https://www.refunder.se/.well-known/oauth-protected-resource/mcp/stats
- Authorization server metadata (RFC 8414): https://www.refunder.se/.well-known/oauth-authorization-server

The protected resource metadata carries `resource`, `authorization_servers`,
`scopes_supported` and `bearer_methods_supported` (`header` - send the token as
`Authorization: Bearer ...`).

The authorization server metadata carries the standard OAuth endpoints plus an `agent_auth`
block: `register_uri`, `claim_uri`, `token_uri`, `revocation_uri`,
`identity_types_supported` (`anonymous`), `credential_types_supported` (`bearer_token`),
and the scopes you hold before and after a claim.

## Step 2 - Pick a method

- **You are an agent registering for yourself** -> Step 3. This is the `anonymous` identity
  type; a human can take ownership later through the claim ceremony in Step 4.
- **You are software acting for a signed-in Refunder member** -> "Member-delegated OAuth"
  below. That is the flow that unlocks https://www.refunder.se/mcp/stats.

Refunder does not accept ID-JAG or verified-email identity assertions. There is no provider
trust list, so do not send them; `identity_types_supported` lists what is real.

## Step 3 - Register

    POST https://www.refunder.se/agent/auth
    Content-Type: application/json

    {
      "type": "anonymous",
      "client_name": "Your agent"
    }

Response (200):

    {
      "registration_id": "reg_...",
      "registration_type": "anonymous",
      "access_token": "rfa_...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "rfr_...",
      "refresh_token_expires_in": 2592000,
      "scope": "agent:catalog",
      "pre_claim_scopes": ["agent:catalog"],
      "post_claim_scopes": ["agent:catalog", "agent:member"],
      "claim_url": "https://www.refunder.se/agent/auth/claim",
      "claim_token": "clm_...",
      "claim_token_expires": "..."
    }

The `access_token` is valid for 60 minutes and grants `agent:catalog`: read
access to the public catalog, which is open anyway, plus your own rate limit bucket instead
of one shared with every anonymous caller on your IP. It unlocks no member data.

The `refresh_token` is your long-lived credential - store it, and see Step 5 for how to
exchange it. It is valid for 30 days and rotates on every use.

`claim_token` is returned exactly once and is only good for the 24 hour claim
window. Hold it in memory for the duration of the ceremony; do not persist it. Once the
window closes it stops working, and the refresh token is what keeps you alive.

## Step 4 - Claim ceremony (optional)

Run this only when a member has asked you to act for them. Skip it otherwise.

### 4a. Start the ceremony

    POST https://www.refunder.se/agent/auth/claim
    Content-Type: application/json

    {
      "claim_token": "clm_...",
      "email": "member@example.com"
    }

Response (200) contains a `claim_attempt` block: `user_code`, `verification_uri`,
`verification_uri_complete`, `expires_in` and `interval`. The email pins the ceremony to one
member, so seeing the code is not enough for a third party to claim you.

### 4b. Hand off to the member

Surface `verification_uri_complete` and `user_code` together, in one message:

> Open this link, sign in to Refunder, and enter this code: **ABCD2345**
> https://www.refunder.se/agent/claim?claim_attempt_token=...

The code travels from you to the member, never the other way. Never ask them to read a code
back to you. They sign in, land on the claim page, see which agent is asking, and type the
code. It is valid for 10 minutes.

### 4c. Poll for completion

    POST https://www.refunder.se/agent/auth/token
    Content-Type: application/json

    {
      "grant_type": "urn:refunder:agent-auth:grant-type:claim",
      "claim_token": "clm_..."
    }

Poll every 5 seconds. While waiting you get `400 authorization_pending`. On
success you get a fresh `access_token` and `refresh_token`, both scoped
`agent:catalog agent:member` and bound to the member who claimed you. Every credential you
held before the claim stops working the moment they finish - drop them and store the new
refresh token.

If the code expires while the claim window is still open you get `400 expired_token`: call
4a again with the same `claim_token` and email for a fresh code. If the whole window has
closed you get `410 claim_expired` and have to start over at Step 3.

## Step 5 - Use and refresh

    GET https://www.refunder.se/api/public/stores?query=elgiganten
    Authorization: Bearer rfa_...

Or connect to the MCP server at https://www.refunder.se/mcp/catalog with the same header.

When the access token expires, exchange your refresh token at the same token endpoint:

    POST https://www.refunder.se/agent/auth/token
    Content-Type: application/json

    {
      "grant_type": "refresh_token",
      "refresh_token": "rfr_..."
    }

You get a new pair back and the old refresh token stops working, so persist the new one
before using it. Do not use the claim ceremony to refresh: `claim_token` dies with the claim
window, and re-registering would throw away the member binding. On
`401 invalid_grant` the refresh token is unknown, already rotated, expired or revoked -
start over at Step 3, which means the member has to claim you again.

## Step 6 - Revoke

    POST https://www.refunder.se/agent/auth/revoke
    Content-Type: application/json

    { "token": "rfa_..., rfr_... or clm_..." }

Revoking is permanent and covers the whole registration. Revoking an unknown or already
revoked token is not an error (RFC 7009).

## Member-delegated OAuth

For software acting on behalf of a signed-in member - this is what https://www.refunder.se/mcp/stats
requires - use plain OAuth 2.1 instead of the agent flow above:

1. Register a client dynamically (RFC 7591), no pre-shared credentials needed:

       POST https://www.refunder.se/oauth/register
       Content-Type: application/json

       {
         "client_name": "Your app",
         "redirect_uris": ["https://your-app.example/callback"],
         "grant_types": ["authorization_code", "refresh_token"],
         "response_types": ["code"],
         "token_endpoint_auth_method": "none"
       }

   The redirect_uri host must be on Refunder's allow list. Public clients hold no secret.

2. Send the member to https://www.refunder.se/oauth/authorize with `response_type=code`, `scope=mcp:use`,
   and a PKCE challenge (`code_challenge_method=S256`).

3. Exchange the code at https://www.refunder.se/oauth/token with `grant_type=authorization_code` and your
   `code_verifier`. You get `access_token`, `refresh_token` and `expires_in`.

4. Call https://www.refunder.se/mcp/stats with `Authorization: Bearer <access_token>`. Refresh with
   `grant_type=refresh_token`.

Access to https://www.refunder.se/mcp/stats is additionally restricted to Refunder staff accounts.

## Public access (no auth)

No token is needed for the public catalog:

- https://www.refunder.se/mcp/catalog: public MCP server (stores, campaigns, in-store products, deeplinks).
  Server card: https://www.refunder.se/.well-known/mcp/server-card.json
- https://www.refunder.se/api/public: public JSON catalog. OpenAPI at https://www.refunder.se/api/public/openapi.json.
- https://www.refunder.se/llms.txt, https://www.refunder.se/.well-known/agent-card.json and
  https://www.refunder.se/.well-known/agent-skills/index.json.

## Errors

- `401` with a `WWW-Authenticate` header on a protected resource: run Step 1 and get a token.
- `400 authorization_pending`: the member has not finished the ceremony. Keep polling at the
  advertised `interval`.
- `400 expired_token`: the user_code expired. Start a new claim attempt (4a).
- `410 claim_expired`: the registration is gone. Start over at Step 3.
- `401 invalid_grant` at https://www.refunder.se/agent/auth/token: the `claim_token` or `refresh_token` is
  unknown, already rotated, expired or revoked.
- `429`: you are over the rate limit. Honour `Retry-After`.
- `400 invalid_grant` at https://www.refunder.se/oauth/token: the code or code_verifier is wrong or expired.
- Registration rejected at https://www.refunder.se/oauth/register: the redirect_uri host is not allow-listed.


## Other Refunder regions

- Poland (PLN): https://www.refunder.pl/auth.md
- International (GBP): https://www.refunder.com/auth.md