# Connect PlateHerder — OAuth integration for OWS and other apps

Issuer: `https://plateherder.com`. Metadata: `https://plateherder.com/.well-known/oauth-authorization-server`.

This release implements registered public clients using authorization code + mandatory PKCE S256. `token_endpoint_auth_method` is `none`: client_id is public, there is no client secret. OWS should still exchange codes and keep tokens on its server. Browser CORS, implicit/password grants, dynamic registration and OpenID Connect ID tokens are not supported. Use GET /api/v1/me for the stable PlateHerder user ID. Existing manually created tokens continue working.

## Registration first

Provide PlateHerder with your app name, HTTPS homepage, privacy URL, exact callback URL(s), and required scopes. The operator registers these before linking can work. Client names and return URLs on consent come from this registration, never from the incoming link. No wildcard redirects. Register development localhost callbacks explicitly; production callbacks must use HTTPS. The existing OWS API integration does not automatically create an OAuth client or select a callback route.

Supported scopes: `plates:read`, `plates:write`, `extract:create`. Read includes private plate notes, tags, nutrition and source metadata. Household data, shopping lists, private YouTube lists and Google credentials are excluded. Request only what your app needs.

## 1. Start from a signed-in OWS session

Create a cryptographically random state (at least 128 bits; 16–512 characters) and PKCE verifier (43–128 RFC 7636 unreserved characters; 32 random bytes encoded base64url works). Store both in a short-lived server session tied to the current OWS user and browser. Use Secure, HttpOnly cookies, SameSite=Lax, and a ten-minute attempt lifetime. Store each attempt separately if supporting multiple tabs; never put a token or verifier in the authorization URL.

Compute `code_challenge = BASE64URL(SHA256(code_verifier))` without padding. Redirect the browser to:

```text
https://plateherder.com/oauth/authorize
  ?response_type=code
  &client_id=REGISTERED_CLIENT_ID
  &redirect_uri=URL_ENCODED_EXACT_CALLBACK
  &scope=plates%3Aread%20plates%3Awrite%20extract%3Acreate
  &state=RANDOM_SESSION_BOUND_STATE
  &code_challenge=BASE64URL_SHA256_VERIFIER
  &code_challenge_method=S256
```

PlateHerder offers Google sign-in/account creation, then a separate Allow access / Cancel screen. Signing in alone does not approve the app. Approval returns `code`, unchanged `state`, and `iss=https://plateherder.com` to the registered callback. Cancellation returns `error=access_denied`, `state`, and `iss`. Invalid clients/return URLs never redirect.

## 2. Validate the callback and exchange once

Require the same signed-in OWS session. Check state against the stored attempt, reject missing/duplicate parameters, require the exact expected issuer, and atomically consume the attempt. Handle cancellation after checking state. Do not trust a user ID or redirect destination supplied in the callback. Remove callback query parameters promptly and set `Referrer-Policy: no-referrer`; do not include analytics or third-party assets on that response.

POST `https://plateherder.com/api/oauth/token` using `application/x-www-form-urlencoded`:

```text
grant_type=authorization_code
client_id=REGISTERED_CLIENT_ID
code=RETURNED_CODE
redirect_uri=EXACT_REGISTERED_CALLBACK
code_verifier=STORED_VERIFIER
```

Do not send a client secret or Authorization header. Codes expire after five minutes and can be redeemed once. The client ID, redirect URI and PKCE verifier must match the authorization request.

Success response:

```json
{
  "access_token": "ph_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "phr_...",
  "refresh_token_expires_in": 2592000,
  "scope": "plates:read plates:write extract:create"
}
```

Keep tokens encrypted/server-only; never in a URL, browser storage or logs. Call GET `/api/v1/me` with the access token and store its `id` against the current OWS user. OWS must define whether users can replace an existing link; do not silently move one PlateHerder account between OWS accounts. A new approval replaces that user's previous grant to this app and invalidates its old credentials.

## 3. Refresh, with one request at a time

POST the same token endpoint with `grant_type=refresh_token`, `client_id`, and `refresh_token`. The returned refresh token replaces the old one atomically in your database. Serialize refreshes for each connection across all serverless instances, not just one process. Refresh shortly before access expiry (for example, with 60 seconds remaining).

Refresh tokens rotate on every use. Replaying a consumed code or refresh token revokes its active connection, including recently issued access tokens. A lost refresh response requires reconnecting; do not blindly retry the old refresh token. Refresh cannot expand scopes. Tokens expire after 30 days without renewal; the connection has a maximum one-year lifetime, then requires fresh user approval. Honor the returned expiry values because the final renewal may be shorter.

OAuth errors use `{ "error": "invalid_grant", "error_description": "..." }`; they differ from v1 resource errors. `invalid_grant` means reconnect. Unknown/mismatched clients and invalid permissions return `invalid_client` / `invalid_scope`. Request bodies are capped at 8 KB. Token/revocation requests are capped at 300 per trusted source IP/hour; 429 includes Retry-After. Approval is capped at 30/user/hour. This is separate from extraction quota; coordinate higher partner traffic before launch.

## 4. Disconnect

Users can visit https://plateherder.com/connections (also linked from Account). Disconnect immediately blocks the grant's access and refresh tokens, without deleting the user's plates.

OWS can POST `/api/oauth/revoke` with form fields `client_id` and `token` (access or refresh token). Revocation disconnects that token's grant. Unknown tokens return 200 without revealing whether they exist. Clear local credentials after disconnect. Historical meal-log snapshots remain in OWS; future library access stops.

## Validation checklist for Claude

- Existing user and new Google account → explicit consent → callback → /me → private library.
- Cancel, expired/mismatched state, wrong issuer and wrong PKCE verifier.
- Scope restrictions, cross-user plate access, refresh rotation and concurrent refresh locking.
- Disconnect in OWS and in PlateHerder; reconnect after revoked/expired credentials.
- No codes, access tokens, refresh tokens, or verifiers in application/access logs.

OWS implements its button, start/callback routes and server-side credential storage. PlateHerder does not modify OWS. Google verification remains pending; this is PlateHerder library authorization, not delegation of Google account permissions. Review source-data use and retention in the actual integration before resuming that review.

Implementation references: [OAuth security BCP](https://www.rfc-editor.org/rfc/rfc9700.html), [PKCE](https://www.rfc-editor.org/rfc/rfc7636.html), [token revocation](https://www.rfc-editor.org/rfc/rfc7009.html).
