Skip to content

OAuth Endpoints

Seed provides OAuth 2.1 endpoints for client authentication and token management. These endpoints are public (no authentication required) and follow RFC standards.

Overview

EndpointMethodAuthenticationPurpose
/oauth/registerPOSTPublicRegister new OAuth client
/oauth/authorizeGETPublicStart authorization flow
/oauth/tokenPOSTPublicExchange code or refresh token
/oauth/revokePOSTPublicRevoke access or refresh token

Complete OAuth 2.1 Flow

POST /oauth/register

Dynamic Client Registration - Register a new OAuth 2.0 client (RFC 7591).

Request

http
POST /oauth/register
Content-Type: application/json

{
  "redirect_uris": [
    "https://app.example.com/callback",
    "http://localhost:3000/callback"
  ],
  "client_name": "My Application",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

Parameters

FieldTypeRequiredDescription
redirect_urisarrayYesCallback URLs (1-10, must use HTTPS except localhost)
client_namestringNoHuman-readable name (default: "OAuth Client")
grant_typesarrayNoGrant types (default: ["authorization_code"])
response_typesarrayNoResponse types (default: ["code"])
token_endpoint_auth_methodstringNoAuth method (default: "none")
software_idstringNoSoftware identifier
software_versionstringNoSoftware version

Response (Success)

Status: 201 Created

json
{
  "client_id": "seed-a1b2c3d4e5f6",
  "client_name": "My Application",
  "redirect_uris": [
    "https://app.example.com/callback",
    "http://localhost:3000/callback"
  ],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "client_id_issued_at": 1702857600,
  "client_secret_expires_at": 0
}

Response Fields:

  • client_id: Generated client identifier (format: seed-{random})
  • client_id_issued_at: Unix timestamp of registration
  • client_secret_expires_at: Always 0 (public client, no secret)
  • Other fields echo the request

Client TTL: 30 days (configurable via DCR_CLIENT_TTL)

Response (Error)

Status: 400 Bad Request

json
{
  "error": "invalid_redirect_uri",
  "error_description": "redirect_uri must use https (except for localhost)"
}

Error Codes

ErrorStatusDescription
invalid_redirect_uri400Redirect URI validation failed
invalid_client_metadata400Other validation errors
server_error500Storage or processing error

Validation Rules

Redirect URIs:

  • Must be valid URL format
  • Must use https:// (exception: localhost can use http://)
  • Must not contain fragment (#)
  • Maximum 10 URIs per client

Grant Types:

  • Allowed: authorization_code, refresh_token
  • Invalid values rejected

Response Types:

  • Allowed: code
  • Invalid values rejected

Token Auth Method:

  • Allowed: none, client_secret_post, client_secret_basic
  • Note: Seed only implements none (public clients)

Example: cURL

bash
curl -X POST https://seed.example.com/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_uris": ["https://app.example.com/callback"],
    "client_name": "My App",
    "grant_types": ["authorization_code", "refresh_token"]
  }'

GET /oauth/authorize

Authorization Endpoint - Start OAuth 2.1 authorization code flow with PKCE.

Request

http
GET /oauth/authorize
  ?response_type=code
  &client_id=seed-a1b2c3d4e5f6
  &redirect_uri=https://app.example.com/callback
  &scope=openid+profile+email
  &state=random-csrf-token
  &code_challenge=BASE64URL_SHA256_HASH
  &code_challenge_method=S256

Parameters

ParameterRequiredDescription
response_typeYesMust be code
client_idYesClient identifier from registration
redirect_uriYesCallback URL (must match registered URI)
scopeNoRequested scopes (e.g., openid profile email)
stateRecommendedCSRF protection token
code_challengeYesPKCE challenge (base64url-encoded SHA-256 of verifier)
code_challenge_methodYesMust be S256

PKCE Flow

1. Generate code verifier:

javascript
const codeVerifier = base64url(randomBytes(32)); // 43 characters
// Example: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

2. Create code challenge:

javascript
const codeChallenge = base64url(sha256(codeVerifier));
// Example: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

3. Include in authorization URL:

?code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256

Response (Success)

Status: 302 Found

http
HTTP/1.1 302 Found
Location: https://auth.example.com/login?...

Server redirects to upstream IdP for authentication. After user authenticates, IdP redirects to redirect_uri:

http
HTTP/1.1 302 Found
Location: https://app.example.com/callback?code=AUTHORIZATION_CODE&state=random-csrf-token

Response (Error)

Status: 302 Found (redirect to redirect_uri with error)

http
HTTP/1.1 302 Found
Location: https://app.example.com/callback?error=invalid_client&state=random-csrf-token

Error Codes

ErrorDescription
invalid_requestMissing or invalid parameter
invalid_clientClient ID not found or expired
unauthorized_clientClient not authorized for this grant type
access_deniedUser denied authorization
unsupported_response_typeresponse_type is not code
invalid_scopeRequested scope is invalid
server_errorInternal server error

Example: Full Flow

javascript
// 1. Generate PKCE verifier
const codeVerifier = crypto.randomBytes(32).toString('base64url');

// 2. Create challenge
const challenge = crypto.createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');

// 3. Build authorization URL
const authUrl = new URL('https://seed.example.com/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'seed-a1b2c3d4e5f6');
authUrl.searchParams.set('redirect_uri', 'https://app.example.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', 'random-state');
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// 4. Redirect user to authUrl
window.location.href = authUrl.toString();

// 5. After callback, exchange code for tokens (see POST /oauth/token)

POST /oauth/token

Token Endpoint - Exchange authorization code or refresh token for access tokens.

Authorization Code Grant

Exchange authorization code for tokens.

Request

http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://app.example.com/callback
&client_id=seed-a1b2c3d4e5f6
&code_verifier=ORIGINAL_PKCE_VERIFIER

Parameters

ParameterRequiredDescription
grant_typeYesMust be authorization_code
codeYesAuthorization code from /oauth/authorize
redirect_uriYesMust match authorization request
client_idYesClient identifier
code_verifierYesOriginal PKCE verifier (plain text)

Response (Success)

Status: 200 OK

json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "scope": "openid profile email"
}

Response Fields:

  • access_token: JWT bearer token for API access
  • token_type: Always Bearer
  • expires_in: Token lifetime in seconds
  • refresh_token: Token for refreshing access token
  • scope: Granted scopes (may differ from requested)

Refresh Token Grant

Exchange refresh token for new access token.

Request

http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=seed-a1b2c3d4e5f6

Parameters

ParameterRequiredDescription
grant_typeYesMust be refresh_token
refresh_tokenYesRefresh token from previous response
client_idYesClient identifier

Response (Success)

Status: 200 OK

json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "scope": "openid profile email"
}

Note: May return a new refresh token (token rotation).

Response (Error)

Status: 400 Bad Request or 401 Unauthorized

json
{
  "error": "invalid_grant",
  "error_description": "Authorization code is invalid or expired"
}

Error Codes

ErrorStatusDescription
invalid_request400Missing required parameter
invalid_client401Client not found or expired
invalid_grant400Invalid code or refresh token
unauthorized_client400Client not authorized for grant type
unsupported_grant_type400Grant type not supported
invalid_scope400Requested scope invalid
server_error500Internal server error

Example: Complete Flow

javascript
// After receiving authorization code from callback
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');

// Verify state matches
if (state !== savedState) {
  throw new Error('CSRF token mismatch');
}

// Exchange code for tokens
const response = await fetch('https://seed.example.com/oauth/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: code,
    redirect_uri: 'https://app.example.com/callback',
    client_id: 'seed-a1b2c3d4e5f6',
    code_verifier: savedCodeVerifier, // From PKCE setup
  }),
});

const tokens = await response.json();
// Store tokens.access_token and tokens.refresh_token

Example: Token Refresh

javascript
// When access token expires, use refresh token
const response = await fetch('https://seed.example.com/oauth/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: storedRefreshToken,
    client_id: 'seed-a1b2c3d4e5f6',
  }),
});

const tokens = await response.json();
// Update stored tokens

Example: cURL

Authorization Code:

bash
curl -X POST https://seed.example.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=$AUTH_CODE" \
  -d "redirect_uri=https://app.example.com/callback" \
  -d "client_id=seed-a1b2c3d4e5f6" \
  -d "code_verifier=$CODE_VERIFIER"

Refresh Token:

bash
curl -X POST https://seed.example.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$REFRESH_TOKEN" \
  -d "client_id=seed-a1b2c3d4e5f6"

POST /oauth/revoke

Token Revocation - Revoke access tokens or refresh tokens (RFC 7009).

Request

http
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=REFRESH_TOKEN_OR_ACCESS_TOKEN
&client_id=seed-a1b2c3d4e5f6
&token_type_hint=refresh_token

Parameters

ParameterRequiredDescription
tokenYesThe token to revoke (access or refresh token)
client_idYesClient identifier
token_type_hintNoToken type hint: access_token or refresh_token

Response (Success)

Status: 200 OK

json
{}

Note: The response is always 200 OK, even if the token was already invalid or revoked. This prevents token scanning attacks.

Response (Error)

Status: 400 Bad Request

json
{
  "error": "invalid_request",
  "error_description": "Missing required parameter: token"
}

Error Codes

ErrorStatusDescription
invalid_request400Missing required parameter
invalid_client401Client not found or expired
unsupported_token_type400Token type not supported
server_error500Internal server error

Example: cURL

bash
# Revoke refresh token
curl -X POST https://seed.example.com/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=$REFRESH_TOKEN" \
  -d "client_id=seed-a1b2c3d4e5f6" \
  -d "token_type_hint=refresh_token"

# Revoke access token
curl -X POST https://seed.example.com/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=$ACCESS_TOKEN" \
  -d "client_id=seed-a1b2c3d4e5f6" \
  -d "token_type_hint=access_token"

Example: JavaScript

javascript
// Revoke refresh token on logout
async function logout(refreshToken, clientId) {
  await fetch('https://seed.example.com/oauth/revoke', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      token: refreshToken,
      client_id: clientId,
      token_type_hint: 'refresh_token',
    }),
  });

  // Clear stored tokens
  localStorage.removeItem('access_token');
  localStorage.removeItem('refresh_token');
}

Use Cases

  1. User logout - Revoke refresh token when user logs out
  2. Security incident - Revoke tokens if device is compromised
  3. Token rotation - Revoke old tokens after successful refresh
  4. Session management - Revoke tokens for inactive sessions

Behavior

Revocation effects:

  • Refresh tokens: Immediately invalidated at IdP, can no longer be used to obtain new access tokens
  • Access tokens: Added to revocation cache (5-minute TTL), rejected by authentication middleware
  • Both tokens: If access token derived from refresh token, both are revoked

Security note: Token revocation is proxied to the upstream IdP. Revocation is effective immediately but depends on IdP implementation.

Rate Limiting

Client Registration (POST /oauth/register) is rate limited:

  • Per-IP Limit: 10 registrations per hour
  • Global Limit: 1,000 registrations per hour
  • Window: 1 hour sliding window
  • Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
  • Error Response: 429 Too Many Requests with Retry-After header

Token Endpoint (POST /oauth/token):

  • No rate limiting enforced by Seed (proxied to upstream IdP)
  • Relies on upstream identity provider rate limits
  • Consider implementing token endpoint rate limiting for production

Authorization Endpoint (GET /oauth/authorize):

  • No rate limiting enforced by Seed (redirected to upstream IdP)
  • Protected by upstream identity provider

For detailed implementation and configuration, see Rate Limiting Architecture.

Security Considerations

Client Registration

  • HTTPS required for redirect URIs (except localhost)
  • No fragments allowed in redirect URIs
  • 30-day client TTL (automatic expiration)
  • Rate limiting prevents abuse

Authorization Flow

  • PKCE required (S256 method only)
  • State parameter recommended for CSRF protection
  • Redirect URI exact match validation
  • Code single-use only (enforced by IdP)

Token Exchange

  • Code verifier must match challenge
  • Refresh tokens are long-lived (manage carefully)
  • Tokens issued by upstream IdP (Seed proxies)

Best Practices

Client Applications

  1. Use PKCE: Always generate cryptographically random verifier
  2. Validate state: Check state parameter matches to prevent CSRF
  3. Secure storage: Store refresh tokens securely (not in localStorage)
  4. Token refresh: Refresh before expiration, not after
  5. Error handling: Implement retry logic for network errors

Security

  1. HTTPS only: Never use HTTP in production (except localhost)
  2. Short-lived access tokens: Configure IdP for 1-hour token lifetime
  3. Rotate refresh tokens: Enable token rotation at IdP
  4. Validate redirect URIs: Register exact URIs, use separate URIs per environment
  5. Monitor registrations: Alert on unusual registration patterns

Testing

Test Client Registration

bash
# Register client
curl -X POST https://seed.example.com/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_uris": ["http://localhost:3000/callback"],
    "client_name": "Test Client"
  }'

Test Authorization Flow

bash
# Build authorization URL (paste in browser)
echo "https://seed.example.com/oauth/authorize?response_type=code&client_id=seed-abc123&redirect_uri=http://localhost:3000/callback&scope=openid&state=test&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256"

Test Token Exchange

bash
# Exchange code (replace variables)
curl -X POST https://seed.example.com/oauth/token \
  -d "grant_type=authorization_code" \
  -d "code=$CODE" \
  -d "redirect_uri=http://localhost:3000/callback" \
  -d "client_id=seed-abc123" \
  -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

Released under the MIT License.