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
- Authentication Flow
- API Endpoints
- JWT Token Structure
- Authorization Headers
- License Tiers
- Error Codes
- Integration Examples
- Security Best Practices
- 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:
- CLI requests a device code from the License API
- API returns
device_code,user_code, andverification_uri - CLI displays the verification URL and user code to the user
- User opens browser, navigates to URL, and enters the code
- CLI polls the API to check if authorization is complete
- API exchanges the device code with GitHub for an access token
- GitHub returns the access token
- API returns the access token along with user info and license tier
- 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.1Host: license.seedsource.devContent-Type: application/jsonNo 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:
| Field | Type | Description |
|---|---|---|
device_code | string | Unique identifier for this auth session (internal use) |
user_code | string | Short code user enters on GitHub (e.g., βWDJB-MJHTβ) |
verification_uri | string | URL where user authorizes the device |
expires_in | integer | Seconds until device_code expires (typically 900s/15min) |
interval | integer | Minimum seconds between polling requests (typically 5s) |
cURL Example:
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.1Host: license.seedsource.devContent-Type: application/x-www-form-urlencoded
device_code=3584d83530557fdd1f46af8289938c8ef79f9dc5&grant_type=urn:ietf:params:oauth:grant-type:device_codeGET Request (Legacy - Supported for backward compatibility):
GET /auth/device/token?device_code=3584d83530557fdd1f46af8289938c8ef79f9dc5 HTTP/1.1Host: license.seedsource.devQuery/Form Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
device_code | string | Yes | The device code from step 1 |
grant_type | string | No | OAuth 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:
| Field | Type | Description |
|---|---|---|
access_token | string | GitHub OAuth access token (use for all API calls) |
email | string | Userβs primary verified GitHub email |
username | string | GitHub username |
tier | string | Highest license tier user has access to (free, alpha, pro, enterprise) |
org_name | string | null | Organization name if tier is from org license |
Error Responses:
| Status | Error | Description |
|---|---|---|
| 428 | authorization_pending | User hasnβt authorized yet, keep polling |
| 404 | Invalid device code | Device code doesnβt exist or expired |
| 400 | expired_token | Device code expired (15 min timeout) |
| 400 | access_denied | User declined authorization |
cURL Examples:
# 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 requestsimport time
device_code = "3584d83530557fdd1f46af8289938c8ef79f9dc5"interval = 5max_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']}") breakGET /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.1Host: license.seedsource.devAuthorization: Bearer gho_qKdD8HFztG5YK3hX9vL2BmN4PwJ6R7sTHeaders:
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | string | Yes | Bearer <github_access_token> |
Response (200 OK):
{ "tier": "pro", "status": "active", "email": "john.doe@example.com", "username": "johndoe", "org_name": "Acme Corporation"}Response Fields:
| Field | Type | Description |
|---|---|---|
tier | string | Current license tier (free, alpha, pro, enterprise) |
status | string | License status (active, expired, suspended) |
email | string | Userβs verified email |
username | string | GitHub username |
org_name | string | null | Organization name if applicable |
Error Responses:
| Status | Error | Description |
|---|---|---|
| 401 | Missing or invalid Authorization header | Token missing or malformed |
| 401 | Invalid GitHub token | Token invalid or revoked |
cURL Example:
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:useranduser:email(GitHub permissions)
Token Storage
Secure Storage Locations:
- macOS: Keychain (
sscli loginuses 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_qKdD8HFztG5YK3hX9vL2BmN4PwJ6R7sTFormat: Bearer <access_token>
Example Authenticated Requests
cURL:
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:
| Tier | Features | Max Users | Max Orgs | Price |
|---|---|---|---|---|
| Free | Basic templates, CLI access | 1 | 0 | $0/mo |
| Alpha | All templates, priority support, alpha features | 5 | 1 | Invite-only |
| Pro | All features, custom templates, integration support | 25 | 3 | $199/mo |
| Enterprise | Unlimited, on-premise, dedicated support, SLA | Unlimited | Unlimited | Custom |
Tier Resolution
When a user belongs to multiple organizations or has both personal and org licenses, the highest tier wins:
Priority Order:
enterprise(Tier 4)pro(Tier 3)alpha(Tier 2)free(Tier 1)
Example Scenario:
- User has personal
freelicense - User is member of βAcme Corpβ with
prolicense - User is member of βStartup Incβ with
alphalicense - Result: User gets
protier (highest available)
Checking Tier in CLI
sscli auth status
# Output:# β
Authenticated as johndoe (john.doe@example.com)# π« License: pro (via Acme Corporation)# β° Status: activeError Codes
HTTP Status Codes
| Status | Meaning | Common Causes |
|---|---|---|
| 200 | OK | Request successful |
| 400 | Bad Request | Invalid parameters or expired device code |
| 401 | Unauthorized | Missing, invalid, or expired token |
| 404 | Not Found | Device code doesnβt exist |
| 428 | Precondition Required | Authorization pending (keep polling) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server-side issue |
Application Error Codes
| Code | Message | Resolution |
|---|---|---|
AUTH_001 | Missing or invalid token | Ensure Authorization: Bearer <token> header present |
AUTH_002 | Invalid device code | Device code expired or doesnβt exist, restart auth flow |
AUTH_003 | Authorization pending | User hasnβt entered code yet, keep polling |
AUTH_004 | Token expired | Re-authenticate with sscli login |
AUTH_005 | Token revoked | User revoked access on GitHub, re-authenticate |
AUTH_006 | Rate limit exceeded | Wait 60 seconds before retrying |
AUTH_007 | Invalid GitHub token | Token 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 python3import requestsimport timeimport jsonfrom 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
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 componentsexport const authService = new AuthService();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
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 endend
# 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
Authorizationheader (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:
| Endpoint | Rate Limit | Window |
|---|---|---|
/auth/device/code | 5 requests | 15 minutes |
/auth/device/token | Follows GitHub interval (5s min) | Per device code |
/auth/validate | 100 requests | 1 minute |
Best Practices:
- Respect the
intervalfield from/auth/device/codewhen polling - Implement exponential backoff on errors
- Cache validation results for 5-10 seconds
- Handle
429 Too Many Requestsgracefully
5. Error Handling
import requestsfrom 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:
# β Wrongcurl https://license.seedsource.dev/auth/validate
# β
Correctcurl https://license.seedsource.dev/auth/validate \ -H "Authorization: Bearer gho_..."2. βInvalid GitHub tokenβ
Cause: Token revoked, expired, or doesnβt exist.
Solution:
# Re-authenticatesscli login
# Or programmaticallypython mycli.py --login3. βAuthorization pendingβ (Status 428)
Cause: User hasnβt completed authorization on GitHub yet.
Solution:
- This is expected behavior during device flow
- Continue polling every
intervalseconds - Timeout after
expires_inseconds (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
intervalfield (minimum 5 seconds between polls) - Check
Retry-Afterheader for wait time - Implement exponential backoff
6. CORS errors in browser
Cause: Browser blocking cross-origin requests.
Solution:
// 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
#!/bin/bashAPI_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 fidoneAutomated Testing (pytest)
import pytestimport requestsfrom 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
- GitHub Device Flow Docs: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow
- OAuth 2.0 Device Flow RFC: https://datatracker.ietf.org/doc/html/rfc8628
- sscli CLI Source: https://github.com/seed-source/stack-cli
- License Server Source: https://github.com/seed-source/license-server
Changelog
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2026-02-28 | Initial API reference documentation |
Need Help?
- π§ Email: support@seedsource.dev
- π¬ Discord: https://discord.gg/seedsource
- π Issues: https://github.com/seed-source/stack-cli/issues