# auth.md — how an agent gets a Bisque credential

You are an agent that wants to publish a presentation on Bisque, list what
your user has published, or read a private one. Reading public and unlisted
presentations needs no credential at all (`GET https://bisque.today/api/presentations/context`);
everything else needs a bearer credential. This file tells you how to get
one, use it, and give it back. It is the `agent_auth.skill` document named
in the authorization-server metadata.

Bisque issues two kinds of credential. Both go in the same header,
`Authorization: Bearer <credential>`, on every call to the REST API
(`https://bisque.today/api/...`) and the MCP server (`https://bisque.today/mcp`).

| Credential          | Prefix          | Who it fits                                              | How you get it                       |
| ------------------- | --------------- | -------------------------------------------------------- | ------------------------------------ |
| API key             | `bisque_live_` | CLIs, scripts, local coding agents                       | The user pastes it, or `bisque login` |
| OAuth access token  | `bisque_oat_`  | Hosted chat surfaces where the user cannot paste a key   | The OAuth flow below                 |

## Discover

Call the MCP server without a credential. The 401 names the resource
metadata; the resource metadata names the authorization server; the
authorization server's metadata carries the `agent_auth` block with every
URI you need.

```http
POST https://bisque.today/mcp
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
```

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://bisque.cloud/.well-known/oauth-protected-resource/presentations/mcp", error="invalid_token"
```

```http
GET https://bisque.cloud/.well-known/oauth-protected-resource
GET https://bisque.cloud/.well-known/oauth-authorization-server
```

The second document contains:

```json
{
  "issuer": "https://bisque.cloud",
  "authorization_endpoint": "https://bisque.cloud/oauth/authorize",
  "token_endpoint": "https://bisque.cloud/oauth/token",
  "registration_endpoint": "https://bisque.cloud/oauth/register",
  "revocation_endpoint": "https://bisque.cloud/oauth/revoke",
  "agent_auth": {
    "skill": "https://bisque.cloud/auth.md",
    "register_uri": "https://bisque.cloud/oauth/register",
    "claim_uri": "https://bisque.cloud/oauth/authorize",
    "revocation_uri": "https://bisque.cloud/oauth/revoke",
    "identity_types_supported": ["anonymous"],
    "anonymous": {
      "credential_types_supported": ["access_token"],
      "claim_flow": "authorization_code"
    }
  }
}
```

## Pick a method

- **You run on the user's machine** (a terminal, a coding agent, a script):
  use an **API key**. Ask the user to open https://bisque.cloud/setup/keys and paste the
  key, or run `bisque login` — it prints a pairing code, the user approves
  it in the browser, and the key lands in the CLI's config. No OAuth.
- **You run in a hosted chat product** (a connector, a custom GPT, a Claude
  connector): use **OAuth**. Bisque supports `anonymous` registration only:
  you register a client with no user identity, then the user claims that
  registration by signing in and approving. Bisque does not accept an
  `identity_assertion` (id-jag) or a verified-email registration; there is
  no way to skip the user's approval.

## Register

Register a client at `register_uri` (RFC 7591 dynamic client registration).
Public clients use `token_endpoint_auth_method: "none"` and PKCE; confidential
clients receive a `client_secret`.

```http
POST https://bisque.cloud/oauth/register
Content-Type: application/json

{
  "client_name": "My agent",
  "redirect_uris": ["https://my-agent.example/oauth/callback"],
  "token_endpoint_auth_method": "none"
}
```

```json
{
  "client_id": "bisque_client_…",
  "client_name": "My agent",
  "redirect_uris": ["https://my-agent.example/oauth/callback"],
  "token_endpoint_auth_method": "none"
}
```

Redirect URIs must be `https`, or `http` on a loopback address. A
registration is anonymous: it is not tied to any user until the claim below.

## Claim

The user claims the registration through the authorization-code flow at
`claim_uri`. Open this URL for the user (PKCE S256 is required):

```http
GET https://bisque.cloud/oauth/authorize?response_type=code&client_id=bisque_client_…&redirect_uri=https://my-agent.example/oauth/callback&scope=presentations&code_challenge=<S256>&code_challenge_method=S256&state=<random>&resource=https://bisque.cloud/presentations/mcp
```

The user signs in to Bisque (Google or Apple) and sees a consent page naming
your `client_name`. On approval the browser returns to your redirect URI with
`code` and `state`. Exchange the code within 10 minutes:

```http
POST https://bisque.cloud/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=bisque_oac_…&code_verifier=<verifier>&redirect_uri=https://my-agent.example/oauth/callback&client_id=bisque_client_…
```

```json
{
  "access_token": "bisque_oat_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "bisque_ort_…",
  "scope": "presentations"
}
```

Scopes: `presentations` (always granted; publish, list, read as the user),
`openid` and `email` (identity via `GET https://bisque.cloud/oauth/userinfo`).

## Use the credential

```http
GET https://bisque.today/api/presentations/status?presentationId=…
Authorization: Bearer bisque_oat_…
```

Access tokens last one hour. When one expires, rotate the refresh token —
the old refresh token is revoked in the same call and a new pair comes back:

```http
POST https://bisque.cloud/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=bisque_ort_…&client_id=bisque_client_…
```

Refresh tokens last 90 days. API keys do not expire; the user rotates or
revokes them at https://bisque.cloud/setup/keys.

The full endpoint list with request and response shapes is the OpenAPI
document at https://bisque.today/openapi.json. The MCP server at https://bisque.today/mcp accepts
either credential and lists a tool set that matches it (API-key callers
also get `publish_narrated_presentation`, for audio synthesized locally).

## Errors

REST errors are JSON: `{"requestId":"…","error":{"code":"…","message":"…"}}`.
OAuth endpoints answer with RFC 6749 bodies: `{"error":"…","error_description":"…"}`.

| Where            | Code                     | Meaning and what to do                                                        |
| ---------------- | ------------------------ | ----------------------------------------------------------------------------- |
| REST / MCP       | HTTP 401, `UNAUTHORIZED` or `INVALID_ACCESS_TOKEN` | The credential is missing, expired, or revoked. Refresh, or restart at Register. |
| REST             | HTTP 429, `CONTEXT_RATE_LIMITED` | Per-IP limit on `/api/presentations/context`. Wait a few minutes.        |
| `/oauth/token`   | `invalid_grant`          | Code used twice, PKCE mismatch, wrong redirect URI, or a revoked/expired refresh token. Restart at Claim. |
| `/oauth/token`   | `invalid_client`         | Unknown `client_id` or wrong secret. Restart at Register.                     |
| `/oauth/token`   | `invalid_request`        | A required field (`code_verifier`, `token`) is missing.                       |
| `/oauth/token`   | `unsupported_grant_type` | Only `authorization_code` and `refresh_token` exist.                          |
| `/oauth/authorize` | `access_denied` in the redirect | The user declined. Do not retry without asking them.                   |

## Revocation

Give a credential back at `revocation_uri` (RFC 7009). Revoking a refresh
token also revokes every access token issued with it. Unknown tokens return
200 — the endpoint never says whether a token existed.

```http
POST https://bisque.cloud/oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=bisque_ort_…&token_type_hint=refresh_token&client_id=bisque_client_…
```

The user can also disconnect your client at any time from https://bisque.cloud/account
("Connected apps"), which revokes every token it holds; your next call gets a
401 and you restart at Claim. API keys are revoked at https://bisque.cloud/setup/keys.
