# Sign-in and tokens

PorchQ's MCP server follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization): OAuth 2.1 with PKCE, discovered from the server itself. Most MCP clients do all of this for you. This page is for anyone building or debugging one.

## The flow at a glance

1. The app calls `/mcp` without a token and gets `401` with a `WWW-Authenticate` header.
2. It reads PorchQ's protected resource metadata, then its authorization server metadata.
3. It identifies itself: with a client ID metadata document (preferred) or by registering.
4. It sends the person's browser to `/oauth/authorize` with a PKCE challenge.
5. The person signs in, chooses communities and permissions, and allows.
6. The app exchanges the code at `/oauth/token` for an access token and a refresh token.
7. It calls `/mcp` with `Authorization: Bearer <access token>`, and refreshes when the token runs out.

## 1. Discovery

Every request to `/mcp` needs a token, including `initialize`, `server/discover` and `tools/list`. Without one, PorchQ answers HTTP `401`:

```http
HTTP/2 401
WWW-Authenticate: Bearer realm="porchq", resource_metadata="https://porchq.com/.well-known/oauth-protected-resource/mcp", scope="community:read community:write maintenance:read maintenance:write documents:read people:read meetings:read billing:read"
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Sign in to PorchQ to use this server."}}
```

The `scope` value is what today's tools need (`community:read`, `community:write`, `maintenance:read`, `maintenance:write`, `documents:read`, `people:read`, `meetings:read`, `billing:read`). Ask for all of it, so one consent covers everything the person may want; they can still untick changes.

**Protected resource metadata** (`GET https://porchq.com/.well-known/oauth-protected-resource/mcp`):

```json
{
  "resource": "https://porchq.com/mcp",
  "authorization_servers": ["https://porchq.com"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["community:read", "community:write", "maintenance:read", "maintenance:write", "documents:read", "people:read", "meetings:read", "billing:read"],
  "resource_name": "PorchQ"
}
```

**Authorization server metadata** (`GET https://porchq.com/.well-known/oauth-authorization-server`):

```json
{
  "issuer": "https://porchq.com",
  "authorization_endpoint": "https://porchq.com/oauth/authorize",
  "token_endpoint": "https://porchq.com/oauth/token",
  "revocation_endpoint": "https://porchq.com/oauth/revoke",
  "introspection_endpoint": "https://porchq.com/oauth/introspect",
  "registration_endpoint": "https://porchq.com/oauth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "client_id_metadata_document_supported": true,
  "token_endpoint_auth_methods_supported": ["none"],
  "scopes_supported": ["read", "write", "community:read", "…", "offline_access"]
}
```

`scopes_supported` lists every [scope](/developers/mcp/permissions), the two older ones (`read`, `write`) and `offline_access`. Apps always get a refresh token, so `offline_access` is accepted and ignored.

## 2. Identify your app

### Client ID metadata document (preferred)

Host a JSON document at an `https` URL you control and use that URL as your `client_id`. PorchQ fetches it when a person is sent to sign in, and shows the person where the details came from.

```json
{
  "client_id": "https://example.com/oauth/porchq-client.json",
  "client_name": "Example Assistant",
  "client_uri": "https://example.com",
  "logo_uri": "https://example.com/logo.png",
  "redirect_uris": ["https://example.com/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

PorchQ's rules for the document:

- The URL is `https`, has a path, and has no user name, password or fragment. It must resolve only to public addresses.
- `client_id` in the document repeats the URL exactly.
- `client_name` is present. `redirect_uris` lists 1 to 10 addresses, each `https` or a loopback address (see below).
- `token_endpoint_auth_method` is `none` or `private_key_jwt`. Both are treated as public clients, protected by PKCE.
- The document is at most 32 KB and answers within 3 seconds, without redirects.
- PorchQ caches it for the `Cache-Control: max-age` you send, between 5 minutes and a day (an hour if you send none).

### Dynamic client registration

If your client can't host a document, register at `POST https://porchq.com/oauth/register` ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)):

```http
POST /oauth/register
Content-Type: application/json

{
  "client_name": "My PorchQ script",
  "redirect_uris": ["http://127.0.0.1:8765/callback"],
  "token_endpoint_auth_method": "none",
  "scope": "community:read community:write maintenance:read maintenance:write"
}
```

```http
HTTP/2 201
Cache-Control: no-store
Content-Type: application/json

{
  "client_id": "Zx1…",
  "client_id_issued_at": 1792188000,
  "client_name": "My PorchQ script",
  "redirect_uris": ["http://127.0.0.1:8765/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "application_type": "web",
  "scope": "community:read community:write maintenance:read maintenance:write"
}
```

- Only public clients register: there is no client secret.
- `scope` lists what the app may ever ask for. Without it, the app may ask for any [scope](/developers/mcp/permissions); the person still decides at sign-in.
- `grant_types` may only be `authorization_code` and `refresh_token`; `response_types` only `code`; `application_type` `web` or `native`.
- A registered app is shown to the person as one that "registered itself with PorchQ", since PorchQ can't confirm who made it.
- Registrations are limited per address; see [Errors and limits](/developers/mcp/errors-and-limits).

### Redirect addresses

A redirect address is `https`, or `http` on the person's own computer: `127.0.0.1`, `[::1]` or `localhost`. For those, any port is accepted, so a command-line tool can listen on whatever port is free. PorchQ warns the person that such an app runs on their own computer.

## 3. Send the person to sign in

Make a PKCE verifier (43 to 128 random characters) and its `S256` challenge. Then open:

```text
https://porchq.com/oauth/authorize
  ?response_type=code
  &client_id=https%3A%2F%2Fexample.com%2Foauth%2Fporchq-client.json
  &redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback
  &scope=community%3Aread+community%3Awrite+maintenance%3Aread+maintenance%3Awrite+documents%3Aread+people%3Aread+meetings%3Aread+billing%3Aread
  &state=af0ifjsldkj
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &resource=https://porchq.com/mcp
```

- `code_challenge_method` must be `S256`.
- `resource` ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707)) names the server the token is for. Send `https://porchq.com/mcp`, or leave it out and PorchQ binds the token to it. Any other value is refused with `invalid_target`.
- Ask for scopes by area. The older `read` and `write` still work and are read as the area scopes they cover.

**What the person sees.** If they aren't signed in, PorchQ asks them to sign in, then brings them back to this page. They must have confirmed their email address. The consent page shows:

- the app's name and where PorchQ learned it (its metadata document's host, or that it registered itself), and where they'll be sent back;
- what the app will be able to see, in plain words, limited to what their roles allow;
- **Let the app make changes**: ticked by default; unticked, the app gets only reading permissions;
- **For these communities**: a box per community they belong to, with their role. At least one must be ticked.

PorchQ never skips this page, even for an app the person allowed before.

**The answer.** PorchQ redirects to your `redirect_uri` with `code`, your `state` and `iss=https://porchq.com` ([RFC 9207](https://www.rfc-editor.org/rfc/rfc9207)). Check that `state` is yours and that `iss` is `https://porchq.com`. If the person cancels, you get `error=access_denied` with the same `state` and `iss`.

What the app is granted is decided by PorchQ, never by the app or the page:

- no changing permissions unless the person allowed changes;
- no owner- or manager-only permissions when every ticked community is one where they are a resident;
- only the communities they ticked;
- never more than the app asked for.

## 4. Exchange the code

```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback
&client_id=https%3A%2F%2Fexample.com%2Foauth%2Fporchq-client.json
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&resource=https://porchq.com/mcp
```

```json
{
  "access_token": "kV3n…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "Qm8t…",
  "scope": "community:read community:write maintenance:read maintenance:write",
  "created_at": 1792188060
}
```

`scope` is what was actually granted. Read it rather than assuming the app got everything it asked for.

## 5. Call the server

Send the access token on every request:

```http
POST /mcp
Authorization: Bearer kV3n…
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
```

A token is accepted only if it is live, was issued for `https://porchq.com/mcp`, belongs to an active PorchQ account with a confirmed email address, and its app has not been turned off. Otherwise PorchQ answers `401` with `error="invalid_token"` in `WWW-Authenticate`; refresh, or sign the person in again.

## 6. Refresh

Access tokens last 60 minutes. Before or when one runs out, refresh:

```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=Qm8t…
&client_id=https%3A%2F%2Fexample.com%2Foauth%2Fporchq-client.json
```

You get a new access token **and a new refresh token**. Keep the new one; the old one is retired.

- **Reuse ends the connection.** If a retired refresh token is used again more than 60 seconds after it was replaced, PorchQ treats it as stolen and ends the whole connection for that app and person. Within those 60 seconds a reused token is only refused, so two refreshes racing each other don't disconnect anyone.
- **Unused connections end.** If an app doesn't refresh for 60 days, the connection ends and the person signs in again.
- A refresh keeps the communities and permissions the person chose.

## 7. More permissions later

If a tool needs a permission the connection doesn't have, PorchQ answers `403` with the missing scopes:

```http
HTTP/2 403
WWW-Authenticate: Bearer realm="porchq", resource_metadata="https://porchq.com/.well-known/oauth-protected-resource/mcp", scope="people:read", error="insufficient_scope"
```

Run step 3 again asking for those scopes. The consent page shows the new lines first, with what's already allowed folded underneath.

## 8. Ending a connection

- **Your app** revokes its tokens at `POST https://porchq.com/oauth/revoke` with `token` and `client_id`. Do this when the person disconnects PorchQ in your app.
- **PorchQ** can turn an app off. A turned-off app gets no consent page and no tokens, and its existing tokens are refused.
- **Time:** reusing a retired refresh token, or not refreshing for 60 days, ends the connection (see step 6).

## Tokens and security

- PorchQ stores codes and tokens as SHA-256 hashes, and shows a token once, in the response that issues it. One issued before 13 October 2026 is rewritten as a hash the first time it is used.
- Keep tokens out of logs, URLs and error reports. Store refresh tokens encrypted.
- Calls from a web page are accepted only from PorchQ, `https://chatgpt.com`, `https://claude.ai` or the person's own computer. Server-side calls send no `Origin` and are unaffected.
