Skip to content

Auth API Reference

Auth API Reference

Overview

The Seed & Source authentication system uses GitHub OAuth Device Flow for secure, passwordless authentication. This approach leverages GitHub’s OAuth infrastructure, eliminating the need to manage passwords while providing enterprise-grade security.

Base URL: https://license.seedsource.dev (Production) Base URL: http://localhost:8000 (Local Development)

Key Features:

  • πŸ” Passwordless authentication via GitHub OAuth
  • 🎫 JWT-like GitHub access tokens
  • πŸ‘₯ Organization & team-based license access
  • πŸ”„ Automatic license tier resolution
  • πŸ“Š Real-time session validation

Table of Contents

  1. Authentication Flow
  2. API Endpoints
  3. JWT Token Structure
  4. Authorization Headers
  5. License Tiers
  6. Error Codes
  7. Integration Examples
  8. Security Best Practices
  9. Troubleshooting

Authentication Flow

The GitHub Device Flow is a multi-step authentication process designed for CLI tools and headless environments.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CLI β”‚ β”‚ License API β”‚ β”‚ GitHub β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
β”‚ β”‚ β”‚
β”‚ 1. Request device code β”‚ β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚ β”‚
β”‚ β”‚ β”‚
β”‚ 2. Return device_code + β”‚ β”‚
β”‚ user_code + verify URL β”‚ β”‚
β”‚<────────────────────────────── β”‚
β”‚ β”‚ β”‚
β”‚ 3. Display URL + user_code β”‚ β”‚
β”‚ to user β”‚ β”‚
β”‚ β”‚ β”‚
β”‚ [User opens browser] β”‚ β”‚
β”‚ [User visits verify URL] β”‚ β”‚
β”‚ [User enters user_code] β”‚ 4. User authorizes β”‚
β”‚ β”‚ ───────────────────────────>β”‚
β”‚ β”‚ β”‚
β”‚ 5. Poll for token β”‚ β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚ β”‚
β”‚ β”‚ β”‚
β”‚ β”‚ 6. Exchange device_code β”‚
β”‚ β”‚ for access_token β”‚
β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚
β”‚ β”‚ β”‚
β”‚ β”‚ 7. Return access_token β”‚
β”‚ β”‚<──────────────────────────────
β”‚ β”‚ β”‚
β”‚ 8. Return access_token + β”‚ β”‚
β”‚ user info + tier β”‚ β”‚
β”‚<────────────────────────────── β”‚
β”‚ β”‚ β”‚
β”‚ 9. Store token locally β”‚ β”‚
β”‚ β”‚ β”‚

Step-by-Step:

  1. CLI requests a device code from the License API
  2. API returns device_code, user_code, and verification_uri
  3. CLI displays the verification URL and user code to the user
  4. User opens browser, navigates to URL, and enters the code
  5. CLI polls the API to check if authorization is complete
  6. API exchanges the device code with GitHub for an access token
  7. GitHub returns the access token
  8. API returns the access token along with user info and license tier
  9. CLI stores the token securely for future requests

API Endpoints

POST /auth/device/code

Description: Initiates the GitHub Device Flow by requesting a device code.

Request:

POST /auth/device/code HTTP/1.1
Host: license.seedsource.dev
Content-Type: application/json

No request body required.

Response (200 OK):

{
"device_code": "3584d83530557fdd1f46af8289938c8ef79f9dc5",
"user_code": "WDJB-MJHT",
"verification_uri": "https://github.com/login/device",
"expires_in": 900,
"interval": 5
}

Response Fields:

FieldTypeDescription
device_codestringUnique identifier for this auth session (internal use)
user_codestringShort code user enters on GitHub (e.g., β€œWDJB-MJHT”)
verification_uristringURL where user authorizes the device
expires_inintegerSeconds until device_code expires (typically 900s/15min)
intervalintegerMinimum seconds between polling requests (typically 5s)

cURL Example:

Terminal window
curl -X POST https://license.seedsource.dev/auth/device/code \
-H "Content-Type: application/json"

Python Example:

import requests
response = requests.post("https://license.seedsource.dev/auth/device/code")
data = response.json()
print(f"Visit: {data['verification_uri']}")
print(f"Enter code: {data['user_code']}")
print(f"Expires in: {data['expires_in']} seconds")

POST /auth/device/token (Recommended) / GET /auth/device/token (Legacy)

Description: Polls for authorization status after user enters the code on GitHub. CLI should call this endpoint repeatedly until authorization succeeds.

POST Request (Recommended - RFC 8628 compliant):

POST /auth/device/token HTTP/1.1
Host: license.seedsource.dev
Content-Type: application/x-www-form-urlencoded
device_code=3584d83530557fdd1f46af8289938c8ef79f9dc5&grant_type=urn:ietf:params:oauth:grant-type:device_code

GET Request (Legacy - Supported for backward compatibility):

GET /auth/device/token?device_code=3584d83530557fdd1f46af8289938c8ef79f9dc5 HTTP/1.1
Host: license.seedsource.dev

Query/Form Parameters:

ParameterTypeRequiredDescription
device_codestringYesThe device code from step 1
grant_typestringNoOAuth grant type (POST only, defaults to urn:ietf:params:oauth:grant-type:device_code)

Response (428 Precondition Required) - Authorization Pending:

{
"detail": "Authorization pending"
}

Response (200 OK) - Authorization Complete:

{
"access_token": "gho_qKdD8HFztG5YK3hX9vL2BmN4PwJ6R7sT",
"email": "john.doe@example.com",
"username": "johndoe",
"tier": "pro",
"org_name": "Acme Corporation"
}

Response Fields:

FieldTypeDescription
access_tokenstringGitHub OAuth access token (use for all API calls)
emailstringUser’s primary verified GitHub email
usernamestringGitHub username
tierstringHighest license tier user has access to (free, alpha, pro, enterprise)
org_namestring | nullOrganization name if tier is from org license

Error Responses:

StatusErrorDescription
428authorization_pendingUser hasn’t authorized yet, keep polling
404Invalid device codeDevice code doesn’t exist or expired
400expired_tokenDevice code expired (15 min timeout)
400access_deniedUser declined authorization

cURL Examples:

Terminal window
# POST method (Recommended - RFC 8628 compliant)
DEVICE_CODE="3584d83530557fdd1f46af8289938c8ef79f9dc5"
curl -X POST "https://license.seedsource.dev/auth/device/token" \
-d "device_code=$DEVICE_CODE&grant_type=urn:ietf:params:oauth:grant-type:device_code"
# GET method (Legacy - still supported)
curl "https://license.seedsource.dev/auth/device/token?device_code=$DEVICE_CODE"

Python Polling Example:

import requests
import time
device_code = "3584d83530557fdd1f46af8289938c8ef79f9dc5"
interval = 5
max_attempts = 180 # 15 minutes / 5 seconds
for attempt in range(max_attempts):
# Use POST (recommended) - RFC 8628 compliant
response = requests.post(
"https://license.seedsource.dev/auth/device/token",
data={
"device_code": device_code,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code"
}
)
# Alternatively, can use GET for backward compatibility:
# response = requests.get(
# "https://license.seedsource.dev/auth/device/token",
# params={"device_code": device_code}
# )
if response.status_code == 200:
data = response.json()
print(f"βœ… Authenticated as {data['username']} ({data['tier']})")
access_token = data["access_token"]
# Store token securely
break
elif response.status_code == 428:
print(f"⏳ Waiting for authorization... ({attempt + 1}/{max_attempts})")
time.sleep(interval)
else:
print(f"❌ Error: {response.json()['detail']}")
break

GET /auth/validate

Description: Validates an access token and returns the user’s current license tier. Used by CLI to check session validity and tier access.

Request:

GET /auth/validate HTTP/1.1
Host: license.seedsource.dev
Authorization: Bearer gho_qKdD8HFztG5YK3hX9vL2BmN4PwJ6R7sT

Headers:

HeaderTypeRequiredDescription
AuthorizationstringYesBearer <github_access_token>

Response (200 OK):

{
"tier": "pro",
"status": "active",
"email": "john.doe@example.com",
"username": "johndoe",
"org_name": "Acme Corporation"
}

Response Fields:

FieldTypeDescription
tierstringCurrent license tier (free, alpha, pro, enterprise)
statusstringLicense status (active, expired, suspended)
emailstringUser’s verified email
usernamestringGitHub username
org_namestring | nullOrganization name if applicable

Error Responses:

StatusErrorDescription
401Missing or invalid Authorization headerToken missing or malformed
401Invalid GitHub tokenToken invalid or revoked

cURL Example:

Terminal window
curl https://license.seedsource.dev/auth/validate \
-H "Authorization: Bearer gho_qKdD8HFztG5YK3hX9vL2BmN4PwJ6R7sT"

Python Example:

import requests
headers = {
"Authorization": "Bearer gho_qKdD8HFztG5YK3hX9vL2BmN4PwJ6R7sT"
}
response = requests.get(
"https://license.seedsource.dev/auth/validate",
headers=headers
)
if response.status_code == 200:
data = response.json()
print(f"Tier: {data['tier']}")
print(f"Status: {data['status']}")
else:
print("❌ Token invalid or expired")

JWT Token Structure

While Seed & Source uses GitHub access tokens (not traditional JWTs), understanding the token structure is important for security.

GitHub Access Token Format

Token Prefix: gho_ (GitHub OAuth token) Example: gho_qKdD8HFztG5YK3hX9vL2BmN4PwJ6R7sT

Characteristics:

  • Length: 40 characters (excluding prefix)
  • Type: Opaque token (not self-contained like JWT)
  • Validation: Server-side only (GitHub API or license server)
  • Expiry: No built-in expiration (revokable via GitHub)
  • Scope: read:user and user:email (GitHub permissions)

Token Storage

Secure Storage Locations:

  • macOS: Keychain (sscli login uses keyring)
  • Linux: Secret Service / gnome-keyring
  • Windows: Windows Credential Locker
  • Fallback: ~/.config/sscli/credentials (file permissions: 0600)

Never store tokens in:

  • Git repositories
  • Environment variables in shared environments
  • Browser localStorage (if building web UI)
  • Unencrypted config files

Authorization Headers

All authenticated API requests must include the Authorization header:

Authorization: Bearer gho_qKdD8HFztG5YK3hX9vL2BmN4PwJ6R7sT

Format: Bearer <access_token>

Example Authenticated Requests

cURL:

Terminal window
curl https://license.seedsource.dev/templates/list \
-H "Authorization: Bearer $SSCLI_TOKEN"

Python requests:

import requests
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(
"https://license.seedsource.dev/templates/list",
headers=headers
)

JavaScript fetch:

const response = await fetch("https://license.seedsource.dev/templates/list", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

Rust reqwest:

let client = reqwest::Client::new();
let response = client
.get("https://license.seedsource.dev/templates/list")
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;

License Tiers

Seed & Source supports four license tiers with different feature access:

TierFeaturesMax UsersMax OrgsPrice
FreeBasic templates, CLI access10$0/mo
AlphaAll templates, priority support, alpha features51Invite-only
ProAll features, custom templates, integration support253$199/mo
EnterpriseUnlimited, on-premise, dedicated support, SLAUnlimitedUnlimitedCustom

Tier Resolution

When a user belongs to multiple organizations or has both personal and org licenses, the highest tier wins:

Priority Order:

  1. enterprise (Tier 4)
  2. pro (Tier 3)
  3. alpha (Tier 2)
  4. free (Tier 1)

Example Scenario:

  • User has personal free license
  • User is member of β€œAcme Corp” with pro license
  • User is member of β€œStartup Inc” with alpha license
  • Result: User gets pro tier (highest available)

Checking Tier in CLI

Terminal window
sscli auth status
# Output:
# βœ… Authenticated as johndoe (john.doe@example.com)
# 🎫 License: pro (via Acme Corporation)
# ⏰ Status: active

Error Codes

HTTP Status Codes

StatusMeaningCommon Causes
200OKRequest successful
400Bad RequestInvalid parameters or expired device code
401UnauthorizedMissing, invalid, or expired token
404Not FoundDevice code doesn’t exist
428Precondition RequiredAuthorization pending (keep polling)
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side issue

Application Error Codes

CodeMessageResolution
AUTH_001Missing or invalid tokenEnsure Authorization: Bearer <token> header present
AUTH_002Invalid device codeDevice code expired or doesn’t exist, restart auth flow
AUTH_003Authorization pendingUser hasn’t entered code yet, keep polling
AUTH_004Token expiredRe-authenticate with sscli login
AUTH_005Token revokedUser revoked access on GitHub, re-authenticate
AUTH_006Rate limit exceededWait 60 seconds before retrying
AUTH_007Invalid GitHub tokenToken doesn’t belong to a GitHub user, re-authenticate

Integration Examples

CLI Authentication Flow (Python)

Full implementation of device flow in a CLI tool:

#!/usr/bin/env python3
import requests
import time
import json
from pathlib import Path
API_BASE = "https://license.seedsource.dev"
TOKEN_FILE = Path.home() / ".config" / "mycli" / "token.json"
def login():
"""Authenticate user via GitHub Device Flow."""
# Step 1: Request device code
response = requests.post(f"{API_BASE}/auth/device/code")
data = response.json()
device_code = data["device_code"]
user_code = data["user_code"]
verification_uri = data["verification_uri"]
expires_in = data["expires_in"]
interval = data["interval"]
# Step 2: Display instructions to user
print("\nπŸ” GitHub Authorization Required")
print(f"πŸ“ Visit: {verification_uri}")
print(f"πŸ”‘ Enter code: {user_code}")
print(f"⏰ Code expires in {expires_in // 60} minutes\n")
# Step 3: Poll for authorization
max_attempts = expires_in // interval
for attempt in range(max_attempts):
time.sleep(interval)
response = requests.post(
f"{API_BASE}/auth/device/token",
data={
"device_code": device_code,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code"
}
)
if response.status_code == 200:
# Success!
auth_data = response.json()
# Save token securely
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
TOKEN_FILE.write_text(json.dumps(auth_data, indent=2))
TOKEN_FILE.chmod(0o600) # Read/write for owner only
print(f"βœ… Authenticated as {auth_data['username']}")
print(f"🎫 License tier: {auth_data['tier']}")
if auth_data.get('org_name'):
print(f"🏒 Organization: {auth_data['org_name']}")
return auth_data["access_token"]
elif response.status_code == 428:
# Still waiting
print(f"⏳ Waiting... ({attempt + 1}/{max_attempts})")
else:
# Error
error = response.json().get("detail", "Unknown error")
print(f"❌ Error: {error}")
return None
print("❌ Timeout: Authorization not completed in time")
return None
def get_token():
"""Get stored token or prompt for login."""
if TOKEN_FILE.exists():
data = json.loads(TOKEN_FILE.read_text())
return data.get("access_token")
return None
def check_auth():
"""Check if current token is valid."""
token = get_token()
if not token:
return False
response = requests.get(
f"{API_BASE}/auth/validate",
headers={"Authorization": f"Bearer {token}"}
)
return response.status_code == 200
def main():
if not check_auth():
print("Not authenticated. Starting login flow...\n")
token = login()
if not token:
print("Authentication failed")
return
else:
print("βœ… Already authenticated")
# Now use the token for API requests
token = get_token()
response = requests.get(
f"{API_BASE}/templates/list",
headers={"Authorization": f"Bearer {token}"}
)
print(f"\nTemplates: {response.json()}")
if __name__ == "__main__":
main()

React Client Integration

src/services/auth.ts
import axios from "axios";
const API_BASE = "https://license.seedsource.dev";
interface DeviceCodeResponse {
device_code: string;
user_code: string;
verification_uri: string;
expires_in: number;
interval: number;
}
interface AuthResponse {
access_token: string;
email: string;
username: string;
tier: string;
org_name: string | null;
}
export class AuthService {
private accessToken: string | null = null;
constructor() {
// Load token from localStorage on init
this.accessToken = localStorage.getItem("sscli_token");
}
async initiateDeviceFlow(): Promise<DeviceCodeResponse> {
const response = await axios.post<DeviceCodeResponse>(`${API_BASE}/auth/device/code`);
return response.data;
}
async pollForToken(
deviceCode: string,
interval: number,
maxAttempts: number
): Promise<AuthResponse> {
for (let i = 0; i < maxAttempts; i++) {
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
try {
const response = await axios.post<AuthResponse>(
`${API_BASE}/auth/device/token`,
{
device_code: deviceCode,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
},
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } }
);
// Success - store token
this.accessToken = response.data.access_token;
localStorage.setItem("sscli_token", this.accessToken);
return response.data;
} catch (error: any) {
if (error.response?.status === 428) {
// Authorization pending, continue polling
continue;
}
// Other error, rethrow
throw error;
}
}
throw new Error("Authorization timeout");
}
async validateToken(): Promise<boolean> {
if (!this.accessToken) return false;
try {
await axios.get(`${API_BASE}/auth/validate`, {
headers: { Authorization: `Bearer ${this.accessToken}` },
});
return true;
} catch {
return false;
}
}
logout(): void {
this.accessToken = null;
localStorage.removeItem("sscli_token");
}
getToken(): string | null {
return this.accessToken;
}
// Create an authenticated Axios instance
getAuthenticatedClient() {
return axios.create({
baseURL: API_BASE,
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
});
}
}
// Usage in components
export const authService = new AuthService();
src/components/Login.tsx
import React, { useState } from "react";
import { authService } from "../services/auth";
export const Login: React.FC = () => {
const [userCode, setUserCode] = useState<string>("");
const [verificationUri, setVerificationUri] = useState<string>("");
const [isPolling, setIsPolling] = useState(false);
const handleLogin = async () => {
try {
// Step 1: Get device code
const deviceData = await authService.initiateDeviceFlow();
setUserCode(deviceData.user_code);
setVerificationUri(deviceData.verification_uri);
setIsPolling(true);
// Step 2: Poll for authorization
const authData = await authService.pollForToken(
deviceData.device_code,
deviceData.interval,
Math.floor(deviceData.expires_in / deviceData.interval)
);
alert(`βœ… Logged in as ${authData.username} (${authData.tier})`);
// Redirect or update UI
window.location.href = "/dashboard";
} catch (error) {
console.error("Login failed:", error);
alert("❌ Login failed");
} finally {
setIsPolling(false);
}
};
return (
<div className="login-container">
<h1>Login with GitHub</h1>
{!isPolling && (
<button onClick={handleLogin} className="btn-primary">
Login
</button>
)}
{isPolling && (
<div className="auth-instructions">
<h2>πŸ” Authorization Required</h2>
<p>
Visit:{" "}
<a href={verificationUri} target="_blank">
{verificationUri}
</a>
</p>
<p>
Enter code: <strong>{userCode}</strong>
</p>
<p>⏳ Waiting for authorization...</p>
</div>
)}
</div>
);
};

Rails API Client Integration

lib/github_auth.rb
require 'httparty'
require 'json'
class GitHubAuth
API_BASE = ENV['LICENSE_API_URL'] || 'https://license.seedsource.dev'
TOKEN_FILE = File.join(Dir.home, '.config', 'myapp', 'token.json')
class << self
def login
# Step 1: Request device code
response = HTTParty.post("#{API_BASE}/auth/device/code")
data = JSON.parse(response.body)
device_code = data['device_code']
user_code = data['user_code']
verification_uri = data['verification_uri']
expires_in = data['expires_in']
interval = data['interval']
# Step 2: Display to user
puts "\nπŸ” GitHub Authorization Required"
puts "πŸ“ Visit: #{verification_uri}"
puts "πŸ”‘ Enter code: #{user_code}"
puts "⏰ Expires in #{expires_in / 60} minutes\n"
# Step 3: Poll for authorization
max_attempts = expires_in / interval
max_attempts.times do |attempt|
sleep interval
response = HTTParty.post("#{API_BASE}/auth/device/token", {
body: {
device_code: device_code,
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
}
})
if response.code == 200
auth_data = JSON.parse(response.body)
save_token(auth_data)
puts "βœ… Authenticated as #{auth_data['username']}"
puts "🎫 License tier: #{auth_data['tier']}"
puts "🏒 Organization: #{auth_data['org_name']}" if auth_data['org_name']
return auth_data['access_token']
elsif response.code == 428
puts "⏳ Waiting... (#{attempt + 1}/#{max_attempts})"
else
error = JSON.parse(response.body)['detail'] rescue 'Unknown error'
puts "❌ Error: #{error}"
return nil
end
end
puts "❌ Timeout: Authorization not completed"
nil
end
def get_token
return nil unless File.exist?(TOKEN_FILE)
data = JSON.parse(File.read(TOKEN_FILE))
data['access_token']
rescue
nil
end
def validate_token
token = get_token
return false unless token
response = HTTParty.get("#{API_BASE}/auth/validate", {
headers: { 'Authorization' => "Bearer #{token}" }
})
response.code == 200
end
def authenticated_request(method, path, options = {})
token = get_token
raise 'Not authenticated' unless token
options[:headers] ||= {}
options[:headers]['Authorization'] = "Bearer #{token}"
HTTParty.send(method, "#{API_BASE}#{path}", options)
end
private
def save_token(data)
FileUtils.mkdir_p(File.dirname(TOKEN_FILE))
File.write(TOKEN_FILE, JSON.pretty_generate(data))
File.chmod(0600, TOKEN_FILE)
end
end
end
# Usage example:
# GitHubAuth.login unless GitHubAuth.validate_token
# response = GitHubAuth.authenticated_request(:get, '/templates/list')

Security Best Practices

1. Token Storage

βœ… DO:

  • Store tokens in system keychains (macOS Keychain, Windows Credential Manager)
  • Use file permissions 0600 (owner read/write only) for config files
  • Encrypt tokens at rest if storing in database
  • Clear tokens on logout

❌ DON’T:

  • Store tokens in Git repositories
  • Log tokens to console or log files
  • Store tokens in browser localStorage (use httpOnly cookies instead)
  • Share tokens between users

2. Token Transmission

βœ… DO:

  • Always use HTTPS in production
  • Send tokens in Authorization header (not query params)
  • Validate SSL certificates
  • Use certificate pinning for mobile apps

❌ DON’T:

  • Send tokens over HTTP (plaintext)
  • Include tokens in URLs (they appear in logs)
  • Expose tokens in client-side JavaScript
  • Disable SSL verification

3. Token Validation

βœ… DO:

  • Validate token on every request (server-side)
  • Check token expiration regularly
  • Implement token revocation
  • Log authentication failures

❌ DON’T:

  • Trust client-side token validation
  • Cache validation results indefinitely
  • Ignore revoked token status
  • Allow unlimited failed auth attempts

4. Rate Limiting

The API implements rate limiting to prevent abuse:

EndpointRate LimitWindow
/auth/device/code5 requests15 minutes
/auth/device/tokenFollows GitHub interval (5s min)Per device code
/auth/validate100 requests1 minute

Best Practices:

  • Respect the interval field from /auth/device/code when polling
  • Implement exponential backoff on errors
  • Cache validation results for 5-10 seconds
  • Handle 429 Too Many Requests gracefully

5. Error Handling

import requests
from time import sleep
def authenticated_request(url, token, max_retries=3):
"""Make authenticated request with retry logic."""
headers = {"Authorization": f"Bearer {token}"}
for attempt in range(max_retries):
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
return response.json()
elif response.status_code == 401:
# Token invalid - re-authenticate required
raise Exception("Token expired or invalid. Please login again.")
elif response.status_code == 429:
# Rate limited - wait and retry
retry_after = response.headers.get('Retry-After', 60)
print(f"Rate limited. Waiting {retry_after}s...")
sleep(int(retry_after))
continue
elif response.status_code >= 500:
# Server error - retry with backoff
wait_time = 2 ** attempt # Exponential backoff
print(f"Server error. Retrying in {wait_time}s...")
sleep(wait_time)
continue
else:
# Other error
raise Exception(f"API error: {response.status_code} - {response.text}")
except requests.exceptions.Timeout:
print(f"Request timeout. Attempt {attempt + 1}/{max_retries}")
if attempt < max_retries - 1:
sleep(2 ** attempt)
continue
raise
raise Exception("Max retries exceeded")

Troubleshooting

Common Issues

1. β€œMissing or invalid Authorization header”

Cause: Token not included in request or malformed header.

Solution:

Terminal window
# ❌ Wrong
curl https://license.seedsource.dev/auth/validate
# βœ… Correct
curl https://license.seedsource.dev/auth/validate \
-H "Authorization: Bearer gho_..."

2. β€œInvalid GitHub token”

Cause: Token revoked, expired, or doesn’t exist.

Solution:

Terminal window
# Re-authenticate
sscli login
# Or programmatically
python mycli.py --login

3. β€œAuthorization pending” (Status 428)

Cause: User hasn’t completed authorization on GitHub yet.

Solution:

  • This is expected behavior during device flow
  • Continue polling every interval seconds
  • Timeout after expires_in seconds (typically 15 minutes)

4. Device code expired

Cause: User took longer than 15 minutes to authorize.

Solution:

  • Restart the auth flow
  • Request a new device code
  • Complete authorization within the time limit

5. Rate limit exceeded (Status 429)

Cause: Too many polling requests.

Solution:

  • Respect the interval field (minimum 5 seconds between polls)
  • Check Retry-After header for wait time
  • Implement exponential backoff

6. CORS errors in browser

Cause: Browser blocking cross-origin requests.

Solution:

backend/routes/auth.js
// Use a backend proxy instead of direct API calls
// Frontend β†’ Your Backend β†’ License API
app.get("/api/auth/validate", async (req, res) => {
const token = req.headers.authorization;
const response = await fetch("https://license.seedsource.dev/auth/validate", {
headers: { Authorization: token },
});
const data = await response.json();
res.json(data);
});

Testing

Testing Device Flow with cURL

test_auth_flow.sh
#!/bin/bash
API_BASE="https://license.seedsource.dev"
echo "Step 1: Request device code"
RESPONSE=$(curl -s -X POST "$API_BASE/auth/device/code")
echo "$RESPONSE" | jq .
DEVICE_CODE=$(echo "$RESPONSE" | jq -r '.device_code')
USER_CODE=$(echo "$RESPONSE" | jq -r '.user_code')
VERIFICATION_URI=$(echo "$RESPONSE" | jq -r '.verification_uri')
INTERVAL=$(echo "$RESPONSE" | jq -r '.interval')
echo ""
echo "Step 2: Authorize on GitHub"
echo "Visit: $VERIFICATION_URI"
echo "Enter code: $USER_CODE"
echo ""
echo "Step 3: Polling for token (press Ctrl+C to stop)..."
while true; do
sleep "$INTERVAL"
TOKEN_RESPONSE=$(curl -s -X POST "$API_BASE/auth/device/token" \
-d "device_code=$DEVICE_CODE&grant_type=urn:ietf:params:oauth:grant-type:device_code")
STATUS=$(echo "$TOKEN_RESPONSE" | jq -r '.detail // "authorized"')
if [ "$STATUS" = "Authorization pending" ]; then
echo "⏳ Waiting..."
elif [ "$STATUS" = "authorized" ]; then
echo "βœ… Authorized!"
echo "$TOKEN_RESPONSE" | jq .
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
echo ""
echo "Step 4: Validate token"
curl -s "$API_BASE/auth/validate" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
break
else
echo "❌ Error: $STATUS"
break
fi
done

Automated Testing (pytest)

tests/test_auth.py
import pytest
import requests
from time import sleep
API_BASE = "https://license.seedsource.dev"
def test_device_code_request():
"""Test requesting a device code."""
response = requests.post(f"{API_BASE}/auth/device/code")
assert response.status_code == 200
data = response.json()
assert "device_code" in data
assert "user_code" in data
assert "verification_uri" in data
assert data["expires_in"] > 0
assert data["interval"] >= 5
def test_device_token_pending():
"""Test polling before user authorizes (should return 428)."""
# Get device code
response = requests.post(f"{API_BASE}/auth/device/code")
device_code = response.json()["device_code"]
# Poll immediately (user hasn't authorized)
response = requests.post(
f"{API_BASE}/auth/device/token",
data={"device_code": device_code, "grant_type": "urn:ietf:params:oauth:grant-type:device_code"}
)
assert response.status_code == 428
assert "pending" in response.json()["detail"].lower()
def test_invalid_device_code():
"""Test with non-existent device code."""
response = requests.post(
f"{API_BASE}/auth/device/token",
data={"device_code": "invalid_code_12345", "grant_type": "urn:ietf:params:oauth:grant-type:device_code"}
)
assert response.status_code == 404
assert "Invalid device code" in response.json()["detail"]
@pytest.mark.skipif(not os.getenv("VALID_TOKEN"), reason="No valid token")
def test_validate_token():
"""Test validating a real token (requires VALID_TOKEN env var)."""
token = os.getenv("VALID_TOKEN")
response = requests.get(
f"{API_BASE}/auth/validate",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
data = response.json()
assert data["tier"] in ["free", "alpha", "pro", "enterprise"]
assert data["status"] == "active"
assert "@" in data["email"]
def test_missing_authorization_header():
"""Test validation without Authorization header."""
response = requests.get(f"{API_BASE}/auth/validate")
assert response.status_code == 401
assert "Missing or invalid" in response.json()["detail"]

Additional Resources


Changelog

VersionDateChanges
1.0.02026-02-28Initial API reference documentation

Need Help?