Skip to main content

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

FieldTypeRequiredDescription
client_namestringyesHuman-readable client name (shown on the consent screen)
redirect_urisstring[]yesAllowed redirect URIs. Must be HTTPS, http://localhost, http://127.0.0.1, or a custom scheme (claude://…)
grant_typesstring[]noDefaults to ["authorization_code", "refresh_token"]
token_endpoint_auth_methodstringno"none" for public/PKCE clients (default), "client_secret_post" for confidential
scopestringnoSpace-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
ParamRequiredNotes
client_idyesFrom DCR
redirect_uriyesMust exact-match a registered URI
response_typeyescode (only supported value)
staterecommendedOpaque CSRF token; echoed in redirect
code_challengeyesBase64url(SHA-256(code_verifier))
code_challenge_methodyesS256 (plain is rejected per OAuth 2.1)
scopeoptionalDefaults 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

ParamRequiredNotes
grant_typeyesauthorization_code
codeyesFrom redirect
redirect_uriyesMust match the authorize request
code_verifieryes43–128 chars; PKCE verifier
client_idyesFrom DCR
client_secretconfidential 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_method must be S256. plain is rejected.
  • code_verifier must be 43–128 characters (letters, digits, -, ., _, ~).
  • Verification is constant-time: base64url(sha256(code_verifier)) is compared against the stored code_challenge via timingSafeEqual.
tip

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

ArtefactLifetimeConstant
Authorization code10 minutesAUTH_CODE_TTL_MS
Access token30 daysACCESS_TOKEN_TTL_MS
Refresh token90 daysREFRESH_TOKEN_TTL_MS