# Connect an app (OAuth 2.1)
URL: https://skedcast.com/developers/oauth

When your app acts on behalf of a SkedCast user, use OAuth 2.1 — the Authorization Code flow with PKCE (S256, mandatory). Clients register dynamically (no manual app setup) and discover every endpoint from the metadata document. This is the end-to-end flow with copy-paste examples. (For your own server-side scripts, an API key is simpler — see Authentication.)

## The flow at a glance
- Discover the authorization server from the well-known metadata.
- Register your client once (Dynamic Client Registration) to get a client_id.
- Generate a PKCE verifier + challenge per authorization.
- Redirect the user to /oauth/authorize; they sign in and consent.
- Exchange the returned code at /oauth/token for an access + refresh token.
- Call the API; refresh when the access token expires; revoke on logout.

## 1. Discover the authorization server
Everything you need is in the RFC 8414 metadata document — endpoints, supported scopes, and the PKCE method. Read endpoints from it rather than hard-coding paths.

## Example
```bash
curl https://api.skedcast.com/.well-known/oauth-authorization-server
```

## 2. Register your client (DCR)
Register once (RFC 7591) to get a client_id. SkedCast issues PUBLIC clients only (PKCE, no client secret). Keep the registration_access_token if you want to update or delete the registration later (RFC 7592).

## redirect_uris must be https (or http loopback for native / CLI clients).
```bash
curl -X POST https://api.skedcast.com/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My App",
    "redirect_uris": ["https://myapp.com/oauth/callback"],
    "scope": "posts.read posts.compose"
  }'
```

## Response — keep client_id; the rest is echoed back.
```json
{
  "client_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "client_id_issued_at": 1751299200,
  "redirect_uris": ["https://myapp.com/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "none",
  "scope": "posts.read posts.compose",
  "registration_access_token": "reg_…",
  "registration_client_uri": "https://api.skedcast.com/oauth/register/f47ac10b-…"
}
```

## 3. Generate a PKCE pair
Per authorization, create a high-entropy code_verifier and its S256 code_challenge (RFC 7636). Keep the verifier private; put the challenge in the authorize URL.

## PKCE (Node) — verifier stays server-side, challenge goes in the URL.
```ts
import { randomBytes, createHash } from "node:crypto";

// code_verifier: 43-128 unreserved chars (RFC 7636 §4.1)
const codeVerifier = randomBytes(32).toString("base64url");

// code_challenge = base64url(SHA-256(verifier)) (RFC 7636 §4.2)
const codeChallenge = createHash("sha256")
  .update(codeVerifier)
  .digest("base64url");
```

## 4. Send the user to authorize
Redirect the user's browser to /oauth/authorize with your client_id, scopes, a random state (CSRF), and the PKCE challenge. They sign in and consent; SkedCast redirects back to your redirect_uri with ?code=…&state=…. Verify state matches what you sent.
Include `resource` to pick which SkedCast resource the token is for — `https://api.skedcast.com/v1` for the REST API (or the MCP server for MCP). Tokens are audience-bound (RFC 8707) and the audience is fixed here, at the authorize step; send the same value at the token step too (it is validated when present, and a mismatch is rejected).

## The authorize URL (line-wrapped for readability).
```http
GET https://api.skedcast.com/oauth/authorize
  ?response_type=code
  &client_id=f47ac10b-…
  &redirect_uri=https://myapp.com/oauth/callback
  &scope=posts.read%20posts.compose
  &state=<random-csrf-token>
  &code_challenge=<codeChallenge>
  &code_challenge_method=S256
  &resource=https://api.skedcast.com/v1
```

## Pick a resource: REST API or MCP
SkedCast has two OAuth-protected resources, each a distinct audience (RFC 8707): the REST API (`https://api.skedcast.com/v1`) and the MCP server (`https://api.skedcast.com/mcp`). A token minted for one is rejected by the other.
Omitting `resource` at the authorize step defaults to the MCP server — a REST caller MUST set `resource=https://api.skedcast.com/v1` there, or the token 401s on /v1. Repeat the same value at the token step: it is validated when present (`invalid_target` on a mismatch); if omitted, the audience captured at authorize time is used.

## 5. Exchange the code for tokens
POST the code back with your code_verifier. SkedCast verifies the PKCE challenge and returns an access + refresh token. Send the same resource you used at the authorize step — the token endpoint validates it when present and rejects a mismatch with invalid_target (the audience itself was fixed at authorize time).

## Example
```bash
curl -X POST https://api.skedcast.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=<authorization_code> \
  -d redirect_uri=https://myapp.com/oauth/callback \
  -d client_id=f47ac10b-… \
  -d code_verifier=<codeVerifier> \
  -d resource=https://api.skedcast.com/v1
```

## Token response.
```json
{
  "access_token": "eyJ…",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "posts.read posts.compose",
  "refresh_token": "eyJ…"
}
```

## 6. Call the API
Send the access token as a Bearer credential — exactly like an API key. Its power is still scopes ∩ the user's role.
The access token must have been issued for the REST API resource (`resource=https://api.skedcast.com/v1` in step 4) — an MCP-only token is rejected here.

## Example
```bash
curl https://api.skedcast.com/v1/agency \
  -H "Authorization: Bearer eyJ…"
```

## 7. Refresh when it expires
Access tokens last 15 minutes. Exchange your refresh token for a new pair before, or when a call returns 401. The refreshed token keeps the same resource as the original and is re-checked against your current role — scopes may narrow if the member was demoted.

## Example
```bash
curl -X POST https://api.skedcast.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=refresh_token \
  -d refresh_token=eyJ… \
  -d client_id=f47ac10b-…
```

## Refresh tokens rotate — store the newest
Each refresh returns a NEW refresh token and invalidates the one you presented (OAuth 2.1 §4.3.1). Always persist the latest. Presenting an already-used refresh token is treated as theft and revokes the entire token family — never reuse one.

## 8. Revoke on logout
When the user disconnects, revoke the refresh token (RFC 7009) — this kills the whole rotation family. The endpoint always returns 200, even for an unknown token, so it never leaks token state.

## Example
```bash
curl -X POST https://api.skedcast.com/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d token=eyJ…
```

## OAuth errors use the RFC 6749 shape
The /oauth/* endpoints return `{ "error": "invalid_grant", "error_description": "…" }` as application/json with a 4xx status — not the /v1 problem+json. Branch on the `error` field.
Common codes: `invalid_request`, `invalid_grant`, `invalid_target` (a resource mismatch — see step 4), `unsupported_grant_type` (a grant we don't implement), `invalid_scope` and `unsupported_response_type` (both returned via the authorize redirect), and `invalid_client_metadata` (a DCR registration with unsupported grant_types/response_types).

## FAQ
**Q: Do I need a client secret?**
A: No. SkedCast registers public clients only and requires PKCE (S256), so there is no client secret to store — the code_verifier proves possession at the token step.

**Q: What is the resource parameter for?**
A: OAuth access tokens are audience-bound (RFC 8707). The resource parameter picks which resource the token is for, and the audience is fixed at the authorize step: to call the REST API you MUST pass resource=https://api.skedcast.com/v1 on /oauth/authorize — omitting it there yields an MCP-only token that 401s on /v1. Repeat the same value on /oauth/token (validated when present; a mismatch is rejected with invalid_target).
