# How sign-in works

> The MCP server uses OAuth 2.1 with PKCE. What a client does to sign in, what the discovery documents say, how long tokens last, and how to disconnect.

When a client such as Claude or Codex connects to `https://api.signwithsigil.com/mcp` without a credential, the server answers `401` with a `WWW-Authenticate` header that points at a small JSON document. From that document the client finds Sigil's authorization server, sends you to a Sigil page to log in and allow the connection, and receives its own tokens. This is the [MCP authorization specification](https://modelcontextprotocol.io/specification/latest/basic/authorization), which is OAuth 2.1 with PKCE. You never paste a key; every connected app can be disconnected on its own.

This page is for people writing an MCP client or CLI of their own, and for anyone who wants to know what the browser round trip is doing.

## The discovery documents

**Protected resource metadata** ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)), at `https://api.signwithsigil.com/mcp`'s host under `/.well-known/oauth-protected-resource/mcp` (and without the `/mcp`), names the resource and its authorization server:

```json
{
  "resource": "https://api.signwithsigil.com/mcp",
  "authorization_servers": ["https://www.signwithsigil.com"],
  "scopes_supported": ["sigil"],
  "bearer_methods_supported": ["header"],
  "resource_name": "Sigil",
  "resource_documentation": "https://docs.signwithsigil.com/mcp"
}
```

**Authorization server metadata** ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)), at `https://www.signwithsigil.com/.well-known/oauth-authorization-server`, lists the endpoints and what they accept:

```json
{
  "issuer": "https://www.signwithsigil.com",
  "authorization_endpoint": "https://www.signwithsigil.com/oauth/authorize",
  "token_endpoint": "https://www.signwithsigil.com/oauth/token",
  "registration_endpoint": "https://www.signwithsigil.com/oauth/register",
  "revocation_endpoint": "https://www.signwithsigil.com/oauth/revoke",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "client_id_metadata_document_supported": true,
  "scopes_supported": ["sigil"],
  "authorization_response_iss_parameter_supported": true
}
```

## Who the client is

Every client is a public client: there is no client secret, and PKCE with `S256` is required on every authorization. A client identifies itself in one of two ways.

**A client ID metadata document** (CIMD). The `client_id` is an `https://` URL of a JSON document the client's maker hosts, which lists the client's name and the redirect URIs it may use. Claude and ChatGPT identify themselves this way, and so does the Sigil CLI with the document at `https://www.signwithsigil.com/oauth/clients/cli.json`. Sigil fetches the document, checks that its `client_id` is the URL it came from, and caches it for a day. The consent page names the document's host, not the name inside it.

**Dynamic client registration** ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)). A client without a hosted document registers once:

```bash
curl https://www.signwithsigil.com/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name":"My agent","redirect_uris":["http://127.0.0.1/callback"],"token_endpoint_auth_method":"none"}'
```

```json title="Answer"
{
  "client_id": "sgc_…",
  "client_name": "My agent",
  "redirect_uris": ["http://127.0.0.1/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "scope": "sigil"
}
```

Redirect URIs must be `https://`, or `http://` on `localhost`, `127.0.0.1` or `[::1]`. A loopback redirect is matched with the port ignored ([RFC 8252 §7.3](https://www.rfc-editor.org/rfc/rfc8252#section-7.3)), because a native app binds whatever port is free; the path and host must still match. A registration unused for 90 days is deleted.

## The round trip

1. The client sends the browser to `https://www.signwithsigil.com/oauth/authorize` with `response_type=code`, its `client_id`, a registered `redirect_uri`, `code_challenge` and `code_challenge_method=S256`, a `state`, and optionally `scope=sigil` and `resource=https://api.signwithsigil.com/mcp`.
2. You log in if you are not, and the consent page shows the app's name, where the browser will be sent back to, and what the app may do. A loopback redirect is pointed out, since it means an app on your own computer.
3. **Allow** sends the browser to the `redirect_uri` with a one-use `code`, the `state`, and `iss=https://www.signwithsigil.com` ([RFC 9207](https://www.rfc-editor.org/rfc/rfc9207)). **Deny** sends `error=access_denied`. A request Sigil cannot send back safely, such as an unknown client or an unregistered redirect, is shown as an error page and never redirected.
4. The client posts the code to the token endpoint, form-encoded:

```bash
curl https://www.signwithsigil.com/oauth/token \
  -d grant_type=authorization_code \
  -d code=sga_… \
  -d code_verifier=… \
  -d redirect_uri=http://127.0.0.1:3118/callback \
  -d client_id=…
```

```json title="Answer"
{
  "access_token": "sgt_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "sgr_…",
  "scope": "sigil"
}
```

The client then sends `Authorization: Bearer sgt_…` on every call to `https://api.signwithsigil.com/mcp`, exactly as it would send an API key. The same token works on the REST API, so the download links a tool returns can be fetched with it.

## Tokens and their lives

| | Lasts | Then |
|---|---|---|
| Access token `sgt_…` | 1 hour | Refresh it. The server answers `401` with the same `WWW-Authenticate` header, and clients refresh on that or shortly before expiry. |
| Refresh token `sgr_…` | 90 days, extended on each use | Sign in again. |
| Authorization code `sga_…` | 10 minutes, one use | A second use revokes the connection it opened. |

Refreshing rotates both tokens:

```bash
curl https://www.signwithsigil.com/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=sgr_… \
  -d client_id=…
```

The old refresh token stops working at once. If it is presented again later, every token of that connection is revoked, because two parties holding one refresh token means it leaked; the real client simply signs in again.

There is one scope, `sigil`, which is everything the account can do through the API: read and create templates and pacts, send, resend and void, download signed documents. It cannot change the password, billing or domains.

## Disconnect

Each connection is listed on the [API page](https://www.signwithsigil.com/api#apps) with when it was made and last used. **Disconnect** revokes it; the app gets `401` on its next call and has to sign in again. A client can also revoke itself ([RFC 7009](https://www.rfc-editor.org/rfc/rfc7009)):

```bash
curl https://www.signwithsigil.com/oauth/revoke -d token=sgr_…
```

Logging out everywhere and closing the account revoke every connection, as they revoke every API key.

## Errors

The token endpoint answers the standard codes ([RFC 6749 §5.2](https://www.rfc-editor.org/rfc/rfc6749#section-5.2)): `invalid_request`, `invalid_grant` (unknown, expired, used or mismatched code or refresh token), `unsupported_grant_type`, with an `error_description` that says which. Only `application/x-www-form-urlencoded` bodies are read; JSON answers `415`. An address that fails many requests in a row is held off for a while.

A `403` from the MCP server after a successful sign-in is the plan, not the token: the account needs Pro or Business.
