OAuth
Storylayer's OAuth implementation is built on the OAuth 2.1 draft with mandatory PKCE, refresh-token rotation, and RFC-7591 dynamic client registration so Claude.ai, ChatGPT, and any hosted AI tool can self-register.
Discovery
Two well-known endpoints describe the auth surface — both return JSON and require no auth.
/.well-known/oauth-authorization-server— RFC 8414 metadata (issuer, endpoints, supported grant types, PKCE methods)./.well-known/oauth-protected-resource— RFC 9728 metadata (resource URI + the authorization servers that can issue tokens for it).
curl https://app.storylayer.ai/.well-known/oauth-authorization-server
{
"issuer": "https://app.storylayer.ai",
"authorization_endpoint": "https://app.storylayer.ai/oauth/authorize",
"token_endpoint": "https://app.storylayer.ai/oauth/token",
"registration_endpoint": "https://app.storylayer.ai/oauth/register",
"revocation_endpoint": "https://app.storylayer.ai/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": [...]
}Dynamic Client Registration
Hosted MCP clients (Claude.ai, ChatGPT) self-register against POST /oauth/register per RFC 7591. We issue public clients only — no client_secret — and require PKCE on every authorization.
POST /oauth/register
content-type: application/json
{
"client_name": "Acme AI Agent",
"redirect_uris": ["https://acme.example/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none",
"scope": "stories:read stories:write moments:read"
}
→ 201
{
"client_id": "cli_...",
"client_id_issued_at": 17...,
"redirect_uris": [...],
"grant_types": ["authorization_code","refresh_token"],
"token_endpoint_auth_method": "none",
"scope": "stories:read stories:write moments:read"
}Authorization code flow (with PKCE)
- Generate a
code_verifier(43–128 char random string) and acode_challenge= base64url(sha256(verifier)). - Send the user to
/oauth/authorize:
https://app.storylayer.ai/oauth/authorize?
response_type=code&
client_id=cli_...&
redirect_uri=https%3A%2F%2Facme.example%2Foauth%2Fcallback&
scope=stories%3Aread%20stories%3Awrite%20moments%3Aread&
state=opaque-csrf-string&
code_challenge=...base64url(sha256(verifier))...&
code_challenge_method=S256- The user signs in (if not already), reviews the requested scopes, and approves. We redirect back with
?code=…&state=…. - Exchange the code for tokens:
POST /oauth/token
content-type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=sl_oac_...
&redirect_uri=https%3A%2F%2Facme.example%2Foauth%2Fcallback
&client_id=cli_...
&code_verifier=...
→ 200
{
"access_token": "sl_oat_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "sl_ort_...",
"scope": "stories:read stories:write moments:read"
}Use the access token on /api/v1/* and /api/mcp exactly like a PAT.
Refresh + revoke
Refresh tokens rotate on every exchange — the response always includes a new refresh token, and the previous one is invalidated. If a refresh fails because the token was already used, treat it as a security event and force the user to re-auth.
POST /oauth/token
grant_type=refresh_token
&refresh_token=sl_ort_...
&client_id=cli_...
→ 200
{
"access_token": "sl_oat_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "sl_ort_..." // NEW token, store this
}Revoke a token (RFC 7009):
POST /oauth/revoke
content-type: application/x-www-form-urlencoded
token=sl_oat_...
&token_type_hint=access_token
&client_id=cli_...
→ 200 (always, even when the token was already invalid)Security notes
- PKCE with
S256is required on every authorization.plainis not supported. - Redirect URIs are matched exactly. Wildcards and partial matches are rejected.
- Authorization codes are single-use and expire in 60 seconds.
- Access tokens expire in 60 minutes by default and can be revoked instantly from the dashboard or via
/oauth/revoke. - Refresh tokens rotate on use; replay attempts are logged and the entire grant is invalidated.