OAuth 2.1 Authorization Server
Site Health implements a minimal OAuth 2.1 Authorization Server so that MCP-aware clients (Claude.ai connectors, future Cursor/Windsurf connectors) can obtain user-scoped access tokens via Dynamic Client Registration and PKCE.
Standards implemented:
- OAuth 2.1 (draft) — authorization code grant with mandatory PKCE (S256).
- RFC 8414 — Authorization Server Metadata.
- RFC 7591 — Dynamic Client Registration.
- RFC 7009 — Token Revocation.
- MCP Authorization spec (2025-03-26).
Base issuer: https://sitehealth.octagramlabs.com.
End-to-end flow
sequenceDiagram
autonumber
participant C as MCP Client (Claude.ai)
participant B as User Browser
participant AS as Site Health AS
participant RS as /api/mcp
C->>RS: POST /api/mcp (no token)
RS-->>C: 401 + WWW-Authenticate: resource_metadata=…
C->>AS: GET /.well-known/oauth-protected-resource
AS-->>C: { authorization_servers: [issuer] }
C->>AS: GET /.well-known/oauth-authorization-server
AS-->>C: RFC 8414 metadata
C->>AS: POST /api/oauth/register (DCR)
AS-->>C: { client_id, redirect_uris }
C->>B: Open /oauth/authorize?client_id=…&code_challenge=…
B->>AS: GET /oauth/authorize
AS->>B: Consent screen
B->>AS: POST consent (approve)
AS-->>B: 303 redirect → redirect_uri?code=…&state=…
B->>C: Delivers code
C->>AS: POST /api/oauth/token (code + code_verifier)
AS-->>C: { access_token, refresh_token, expires_in }
C->>RS: POST /api/mcp with Bearer access_token
RS-->>C: 200 + tool result
Discovery
Authorization Server metadata
GET /.well-known/oauth-authorization-server
Response 200 (cached 1 hour):
{
"issuer": "https://sitehealth.octagramlabs.com",
"authorization_endpoint": "https://sitehealth.octagramlabs.com/oauth/authorize",
"token_endpoint": "https://sitehealth.octagramlabs.com/api/oauth/token",
"registration_endpoint": "https://sitehealth.octagramlabs.com/api/oauth/register",
"revocation_endpoint": "https://sitehealth.octagramlabs.com/api/oauth/revoke",
"scopes_supported": ["mcp:read"],
"response_types_supported": ["code"],
"response_modes_supported": ["query"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
"code_challenge_methods_supported": ["S256"],
"service_documentation": "https://sitehealth.octagramlabs.com/docs/mcp"
}
Protected Resource metadata
GET /.well-known/oauth-protected-resource
Response 200:
{
"resource": "https://sitehealth.octagramlabs.com/api/mcp",
"authorization_servers": ["https://sitehealth.octagramlabs.com"],
"scopes_supported": ["mcp:read"],
"bearer_methods_supported": ["header"],
"resource_documentation": "https://sitehealth.octagramlabs.com/docs/mcp"
}
Dynamic Client Registration (RFC 7591)
POST /api/oauth/register
Content-Type: application/json
Request schema
| Field | Type | Required | Description |
|---|---|---|---|
client_name | string | yes | Human-readable client name (shown on the consent screen) |
redirect_uris | string[] | yes | Allowed redirect URIs. Must be HTTPS, http://localhost, http://127.0.0.1, or a custom scheme (claude://…) |
grant_types | string[] | no | Defaults to ["authorization_code", "refresh_token"] |
token_endpoint_auth_method | string | no | "none" for public/PKCE clients (default), "client_secret_post" for confidential |
scope | string | no | Space-delimited; must be a subset of mcp:read |
Example
curl -X POST https://sitehealth.octagramlabs.com/api/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP App",
"redirect_uris": ["https://myapp.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none"
}'
Response 201:
{
"client_id": "c_4f7a2e9b1d…",
"client_name": "My MCP App",
"redirect_uris": ["https://myapp.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none",
"client_id_issued_at": 1760793600
}
Public (PKCE) clients receive no client_secret. Confidential clients get a client_secret shown once.
Status codes: 201 Created, 400 (invalid redirect URI), 422 (validation error).
Authorization endpoint
GET /oauth/authorize?client_id=…&redirect_uri=…&response_type=code
&state=…&code_challenge=…&code_challenge_method=S256&scope=mcp:read
| Param | Required | Notes |
|---|---|---|
client_id | yes | From DCR |
redirect_uri | yes | Must exact-match a registered URI |
response_type | yes | code (only supported value) |
state | recommended | Opaque CSRF token; echoed in redirect |
code_challenge | yes | Base64url(SHA-256(code_verifier)) |
code_challenge_method | yes | S256 (plain is rejected per OAuth 2.1) |
scope | optional | Defaults to mcp:read |
The user sees a consent screen with the client name and requested scope. On approval, the server issues a 10-minute authorization code and redirects:
HTTP/1.1 303 See Other
Location: https://myapp.example.com/oauth/callback?code=ac_abc123&state=xyz
On denial or error:
Location: …?error=access_denied&error_description=…&state=xyz
Token endpoint
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization Code grant
| Param | Required | Notes |
|---|---|---|
grant_type | yes | authorization_code |
code | yes | From redirect |
redirect_uri | yes | Must match the authorize request |
code_verifier | yes | 43–128 chars; PKCE verifier |
client_id | yes | From DCR |
client_secret | confidential only | — |
curl -X POST https://sitehealth.octagramlabs.com/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=ac_abc123" \
-d "redirect_uri=https://myapp.example.com/oauth/callback" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk…" \
-d "client_id=c_4f7a2e9b1d"
Response 200:
{
"access_token": "at_eyJ0eXAi…",
"token_type": "Bearer",
"expires_in": 2592000,
"refresh_token": "rt_9Zk3p2x…",
"scope": "mcp:read"
}
Refresh Token grant
curl -X POST https://sitehealth.octagramlabs.com/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=rt_9Zk3p2x…" \
-d "client_id=c_4f7a2e9b1d"
Returns a fresh access_token (30d) and a rotated refresh_token (90d from issue).
Error response
{ "error": "invalid_grant", "error_description": "Code expired or already used" }
Status codes: 200 OK, 400 invalid_request / invalid_grant / invalid_client, 401 (bad client secret).
Revocation (RFC 7009)
POST /api/oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=at_eyJ0eXAi…&token_type_hint=access_token
Always returns 200 OK (even if the token was already revoked — per RFC 7009).
PKCE requirements
code_challenge_methodmust beS256.plainis rejected.code_verifiermust be 43–128 characters (letters, digits,-,.,_,~).- Verification is constant-time:
base64url(sha256(code_verifier))is compared against the storedcode_challengeviatimingSafeEqual.
If you're using a well-known OAuth library (openid-client, appauth-js, AppAuth-iOS), PKCE is handled for you — set code_challenge_method=S256 and ship.
TTLs at a glance
| Artefact | Lifetime | Constant |
|---|---|---|
| Authorization code | 10 minutes | AUTH_CODE_TTL_MS |
| Access token | 30 days | ACCESS_TOKEN_TTL_MS |
| Refresh token | 90 days | REFRESH_TOKEN_TTL_MS |