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
| Endpoint | Method | Authentication | Purpose |
|---|---|---|---|
/oauth/register | POST | Public | Register new OAuth client |
/oauth/authorize | GET | Public | Start authorization flow |
/oauth/token | POST | Public | Exchange code or refresh token |
/oauth/revoke | POST | Public | Revoke 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
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
| Field | Type | Required | Description |
|---|---|---|---|
redirect_uris | array | Yes | Callback URLs (1-10, must use HTTPS except localhost) |
client_name | string | No | Human-readable name (default: "OAuth Client") |
grant_types | array | No | Grant types (default: ["authorization_code"]) |
response_types | array | No | Response types (default: ["code"]) |
token_endpoint_auth_method | string | No | Auth method (default: "none") |
software_id | string | No | Software identifier |
software_version | string | No | Software version |
Response (Success)
Status: 201 Created
{
"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 registrationclient_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
{
"error": "invalid_redirect_uri",
"error_description": "redirect_uri must use https (except for localhost)"
}Error Codes
| Error | Status | Description |
|---|---|---|
invalid_redirect_uri | 400 | Redirect URI validation failed |
invalid_client_metadata | 400 | Other validation errors |
server_error | 500 | Storage or processing error |
Validation Rules
Redirect URIs:
- Must be valid URL format
- Must use
https://(exception: localhost can usehttp://) - 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
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
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=S256Parameters
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code |
client_id | Yes | Client identifier from registration |
redirect_uri | Yes | Callback URL (must match registered URI) |
scope | No | Requested scopes (e.g., openid profile email) |
state | Recommended | CSRF protection token |
code_challenge | Yes | PKCE challenge (base64url-encoded SHA-256 of verifier) |
code_challenge_method | Yes | Must be S256 |
PKCE Flow
1. Generate code verifier:
const codeVerifier = base64url(randomBytes(32)); // 43 characters
// Example: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"2. Create code challenge:
const codeChallenge = base64url(sha256(codeVerifier));
// Example: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"3. Include in authorization URL:
?code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256Response (Success)
Status: 302 Found
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/1.1 302 Found
Location: https://app.example.com/callback?code=AUTHORIZATION_CODE&state=random-csrf-tokenResponse (Error)
Status: 302 Found (redirect to redirect_uri with error)
HTTP/1.1 302 Found
Location: https://app.example.com/callback?error=invalid_client&state=random-csrf-tokenError Codes
| Error | Description |
|---|---|
invalid_request | Missing or invalid parameter |
invalid_client | Client ID not found or expired |
unauthorized_client | Client not authorized for this grant type |
access_denied | User denied authorization |
unsupported_response_type | response_type is not code |
invalid_scope | Requested scope is invalid |
server_error | Internal server error |
Example: Full Flow
// 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
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_VERIFIERParameters
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be authorization_code |
code | Yes | Authorization code from /oauth/authorize |
redirect_uri | Yes | Must match authorization request |
client_id | Yes | Client identifier |
code_verifier | Yes | Original PKCE verifier (plain text) |
Response (Success)
Status: 200 OK
{
"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 accesstoken_type: AlwaysBearerexpires_in: Token lifetime in secondsrefresh_token: Token for refreshing access tokenscope: Granted scopes (may differ from requested)
Refresh Token Grant
Exchange refresh token for new access token.
Request
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=seed-a1b2c3d4e5f6Parameters
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be refresh_token |
refresh_token | Yes | Refresh token from previous response |
client_id | Yes | Client identifier |
Response (Success)
Status: 200 OK
{
"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
{
"error": "invalid_grant",
"error_description": "Authorization code is invalid or expired"
}Error Codes
| Error | Status | Description |
|---|---|---|
invalid_request | 400 | Missing required parameter |
invalid_client | 401 | Client not found or expired |
invalid_grant | 400 | Invalid code or refresh token |
unauthorized_client | 400 | Client not authorized for grant type |
unsupported_grant_type | 400 | Grant type not supported |
invalid_scope | 400 | Requested scope invalid |
server_error | 500 | Internal server error |
Example: Complete Flow
// 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_tokenExample: Token Refresh
// 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 tokensExample: cURL
Authorization Code:
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:
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
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=REFRESH_TOKEN_OR_ACCESS_TOKEN
&client_id=seed-a1b2c3d4e5f6
&token_type_hint=refresh_tokenParameters
| Parameter | Required | Description |
|---|---|---|
token | Yes | The token to revoke (access or refresh token) |
client_id | Yes | Client identifier |
token_type_hint | No | Token type hint: access_token or refresh_token |
Response (Success)
Status: 200 OK
{}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
{
"error": "invalid_request",
"error_description": "Missing required parameter: token"
}Error Codes
| Error | Status | Description |
|---|---|---|
invalid_request | 400 | Missing required parameter |
invalid_client | 401 | Client not found or expired |
unsupported_token_type | 400 | Token type not supported |
server_error | 500 | Internal server error |
Example: cURL
# 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
// 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
- User logout - Revoke refresh token when user logs out
- Security incident - Revoke tokens if device is compromised
- Token rotation - Revoke old tokens after successful refresh
- 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-Afterheader
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
- Use PKCE: Always generate cryptographically random verifier
- Validate state: Check state parameter matches to prevent CSRF
- Secure storage: Store refresh tokens securely (not in localStorage)
- Token refresh: Refresh before expiration, not after
- Error handling: Implement retry logic for network errors
Security
- HTTPS only: Never use HTTP in production (except localhost)
- Short-lived access tokens: Configure IdP for 1-hour token lifetime
- Rotate refresh tokens: Enable token rotation at IdP
- Validate redirect URIs: Register exact URIs, use separate URIs per environment
- Monitor registrations: Alert on unusual registration patterns
Testing
Test Client Registration
# 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
# 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
# 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"Related Documentation
- Authentication Endpoints - Using access tokens
- Discovery Endpoints - OAuth metadata
- OAuth 2.1 Implementation - Implementation details
- Session Management - Client storage in Redis