# Auth.md

How to authenticate with Playnix.

Playnix is readable without credentials. Searching, browsing, opening a game
and reading the catalog need nothing at all — a child asking an assistant to
find a co-op platformer should not have to sign in, and neither should the
assistant.

Credentials are needed only for the verbs that write: building a game, liking
one, and publishing.

## Two ways in

### OAuth 2.1 with PKCE — for agents and MCP clients

| | |
| --- | --- |
| Protected resource metadata | [/.well-known/oauth-protected-resource](https://playnix.dev/.well-known/oauth-protected-resource) |
| Authorization server metadata | [/.well-known/oauth-authorization-server](https://playnix.dev/.well-known/oauth-authorization-server) |
| Authorize | `POST /api/gen/oauth/authorize` |
| Token | `POST /api/gen/oauth/token` |
| Dynamic client registration | `POST /api/gen/oauth/register` |
| Scopes | `games:read`, `games:write` |
| PKCE | `S256` required. There are no client secrets for public clients. |

An unauthenticated call to a tool that needs an account does **not** fail at
the transport. It comes back as a normal tool result with `isError: true` and
a sentence explaining how to sign in, because a model has to be able to read
the refusal and act on it.

A 401 from the HTTP API carries `WWW-Authenticate` pointing at the resource
metadata above, as RFC 9728 requires.

#### The whole flow, end to end

Nothing below needs a human except step 3, and that is the point of step 3.

**1. Register yourself.** No credentials, no portal, no waiting. A client id
is a name, not a permission.

```http
POST /api/gen/oauth/register HTTP/1.1
Host: playnix.dev
Content-Type: application/json

{"client_name": "My Agent", "redirect_uris": ["http://127.0.0.1:7777/callback"]}
```

```json
{
  "client_id": "cl_7Qm2…",
  "client_name": "My Agent",
  "redirect_uris": ["http://127.0.0.1:7777/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}
```

You get no client secret, because a client that runs on somebody's laptop
cannot keep one. `http://` is accepted for loopback addresses only; anything
on the open web must be `https://`.

**2. Make a PKCE pair.**

```
code_verifier  = 43-128 random URL-safe characters
code_challenge = BASE64URL(SHA256(code_verifier))
```

**3. Send the person to approve it.** Open this in their browser:

```
https://playnix.dev/api/gen/oauth/authorize
  ?response_type=code
  &client_id=cl_7Qm2…
  &redirect_uri=http://127.0.0.1:7777/callback
  &scope=games:read%20games:write
  &state=<random>
  &code_challenge=<challenge>
  &code_challenge_method=S256
```

They see what your client is called and what it will be allowed to do, and
they sign in with their name and PIN. `code_challenge_method=S256` is
required — not negotiated.

**4. Catch the redirect.**

```
http://127.0.0.1:7777/callback?code=cd_9xK…&state=<random>&iss=https://playnix.dev
```

Check `state` matches what you sent, and `iss` is `https://playnix.dev`.

**5. Exchange the code.** Within sixty seconds, once.

```http
POST /api/gen/oauth/token HTTP/1.1
Host: playnix.dev
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=cd_9xK…&client_id=cl_7Qm2…
&redirect_uri=http://127.0.0.1:7777/callback&code_verifier=<verifier>
```

```json
{"access_token": "…", "token_type": "Bearer", "expires_in": 7776000, "scope": "games:read games:write"}
```

A code is spent the moment it is presented, whether or not the exchange
succeeds — a code that survived a failed attempt could be retried by whoever
stole it.

**6. Use it.**

```http
POST /api/gen/mcp HTTP/1.1
Host: playnix.dev
Authorization: Bearer <access_token>
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"build_game","arguments":{"prompt":"a four player bomb arena"}}}
```

**7. Give it back when you are done.**

```http
POST /api/gen/oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=<access_token>
```

### A name and a PIN — for people, and for scripts

```
POST https://playnix.dev/api/gen/auth/login
{"name": "Mila", "pin": "8213"}
```

Returns an opaque, revocable session token. Send it as
`Authorization: Bearer <token>`.

A PIN rather than a password because the people using this are children: four
digits is something a seven-year-old can remember, and asking a child for an
email address is what turns a children's site into a compliance problem. What
makes four digits safe is not the secret, it is the lockout — five wrong tries
and the account is shut for fifteen minutes. PINs are stored as scrypt hashes
with a per-account salt. Signing out revokes the token on the server; changing
a PIN revokes every token on the account.

Registering with an email instead of a name works the same way and is
optional — it exists so an account can be recovered, not so anyone can be
contacted.

## What an identity gets you

| | guest | signed in |
| --- | --- | --- |
| Search, browse, open a game | yes | yes |
| Build a game | no | yes |
| Like, publish | no | yes |
| Builds per hour | 6, counted per address | 30, counted per account |

Ownership always comes from the token. A request body cannot claim to be
somebody else — that was possible once, and it meant a player's own games
were invisible to them.
