Security Architecture
Orquestra implements a defense-in-depth security model with multiple layers of protection:
Authentication Layers
1. GitHub OAuth 2.0
Used for user authentication in the dashboard:
Implementation:
import { Hono } from 'hono'
import { generateJWT } from '../services/jwt'
const auth = new Hono ()
// Initiate OAuth flow
auth . get ( '/github' , ( c ) => {
const clientId = c . env . GITHUB_OAUTH_ID
const redirectUri = ` ${ c . env . API_BASE_URL } /auth/github/callback`
const scope = 'read:user user:email'
const authUrl = `https://github.com/login/oauth/authorize?` +
`client_id= ${ clientId } &` +
`redirect_uri= ${ encodeURIComponent ( redirectUri ) } &` +
`scope= ${ encodeURIComponent ( scope ) } `
return c . redirect ( authUrl )
})
// Handle OAuth callback
auth . get ( '/callback' , async ( c ) => {
const code = c . req . query ( 'code' )
if ( ! code ) {
return c . json ({ error: 'Missing authorization code' }, 400 )
}
try {
// Exchange code for access token
const tokenResponse = await fetch ( 'https://github.com/login/oauth/access_token' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'Accept' : 'application/json' ,
},
body: JSON . stringify ({
client_id: c . env . GITHUB_OAUTH_ID ,
client_secret: c . env . GITHUB_OAUTH_SECRET ,
code ,
}),
})
const tokenData = await tokenResponse . json ()
const accessToken = tokenData . access_token
// Fetch user data
const userResponse = await fetch ( 'https://api.github.com/user' , {
headers: {
'Authorization' : `Bearer ${ accessToken } ` ,
'Accept' : 'application/vnd.github.v3+json' ,
},
})
const userData = await userResponse . json ()
// Create or update user in database
const db = c . env . DB
await db
. prepare (
`INSERT INTO users (id, username, email, avatar_url, github_id)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(github_id) DO UPDATE SET
username = excluded.username,
email = excluded.email,
avatar_url = excluded.avatar_url,
updated_at = datetime('now')`
)
. bind (
crypto . randomUUID (),
userData . login ,
userData . email ,
userData . avatar_url ,
userData . id
)
. run ()
// Generate JWT token
const jwt = await generateJWT (
{
sub: userData . id . toString (),
username: userData . login ,
},
c . env . JWT_SECRET ,
7 * 24 * 60 * 60 // 7 days
)
// Redirect to frontend with token
const frontendUrl = c . env . FRONTEND_URL
return c . redirect ( ` ${ frontendUrl } /auth/callback?token= ${ jwt } ` )
} catch ( error ) {
console . error ( 'OAuth error:' , error )
return c . redirect ( ` ${ c . env . FRONTEND_URL } /auth/error` )
}
})
export default auth
2. JWT Token Authentication
Used for user sessions :
Token Format:
Header.Payload.Signature (HS256)
Payload:
{
"sub" : "12345" , // User ID
"username" : "alice" , // GitHub username
"iat" : 1703001600 , // Issued at
"exp" : 1703606400 // Expires in 7 days
}
Middleware Implementation:
import { Context , Next } from 'hono'
import { verifyJWT } from '../services/jwt'
export async function authMiddleware ( c : Context , next : Next ) {
const authHeader = c . req . header ( 'Authorization' )
if ( ! authHeader || ! authHeader . startsWith ( 'Bearer ' )) {
return c . json (
{
error: 'Unauthorized' ,
message: 'Missing or invalid Authorization header'
},
401
)
}
const token = authHeader . slice ( 7 )
try {
const payload = await verifyJWT ( token , c . env . JWT_SECRET )
// Set user context for downstream handlers
c . set ( 'userId' , payload . sub )
c . set ( 'username' , payload . username )
c . set ( 'jwtPayload' , payload )
await next ()
} catch ( err ) {
return c . json (
{
error: 'Unauthorized' ,
message: 'Invalid or expired token'
},
401
)
}
}
Usage:
import { authMiddleware } from './middleware/auth'
// Protect routes
app . get ( '/api/idl/upload' , authMiddleware , async ( c ) => {
const userId = c . get ( 'userId' ) // Available after auth
// ... handle upload
})
3. API Key Authentication
Used for programmatic access :
Key Format:
ork_1a2b3c4d5e6f7g8h9i0j (prefix: ork_)
Storage:
CREATE TABLE api_keys (
id TEXT PRIMARY KEY ,
project_id TEXT NOT NULL ,
key TEXT UNIQUE NOT NULL ,
label TEXT ,
expires_at DATETIME ,
last_used DATETIME ,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects(id)
);
Middleware:
export async function apiKeyMiddleware ( c : Context , next : Next ) {
const apiKey = c . req . header ( 'X-API-Key' )
if ( ! apiKey ) {
return c . json (
{ error: 'Unauthorized' , message: 'Missing X-API-Key header' },
401
)
}
const db = c . env . DB
const result = await db
. prepare (
`SELECT ak.*, p.user_id, p.name as project_name
FROM api_keys ak
JOIN projects p ON ak.project_id = p.id
WHERE ak.key = ?
AND (ak.expires_at IS NULL OR ak.expires_at > datetime('now'))`
)
. bind ( apiKey )
. first ()
if ( ! result ) {
return c . json (
{ error: 'Unauthorized' , message: 'Invalid or expired API key' },
401
)
}
// Update last_used (non-blocking)
c . executionCtx . waitUntil (
db . prepare ( 'UPDATE api_keys SET last_used = datetime( \' now \' ) WHERE key = ?' )
. bind ( apiKey )
. run ()
)
c . set ( 'apiKeyProjectId' , result . project_id )
c . set ( 'apiKeyUserId' , result . user_id )
await next ()
}
CORS Configuration
Cross-Origin Resource Sharing is configured to allow only trusted origins:
import { cors } from 'hono/cors'
app . use (
'*' ,
cors ({
origin : ( origin : string ) => {
const allowedOrigins = [
'https://orquestra.dev' ,
'http://localhost:3000' ,
'http://localhost:5173' ,
]
return allowedOrigins . includes ( origin ) ? origin : allowedOrigins [ 0 ]
},
allowHeaders: [ 'Content-Type' , 'Authorization' , 'X-API-Key' ],
allowMethods: [ 'GET' , 'POST' , 'PUT' , 'DELETE' , 'OPTIONS' ],
credentials: true ,
maxAge: 86400 , // 24 hours
})
)
Headers Set:
Access-Control-Allow-Origin: https://orquestra.dev
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Validation Service
Lightweight validation without heavy dependencies:
export interface ValidationError {
field : string
message : string
}
export interface ValidationResult < T > {
success : boolean
data ?: T
errors ?: ValidationError []
}
const BASE58_REGEX = / ^ [ 1-9A-HJ-NP-Za-km-z ] {32,44} $ /
/**
* Validate transaction build request
*/
export function validateBuildRequest ( body : unknown ) : ValidationResult < any > {
const errors : ValidationError [] = []
if ( typeof body !== 'object' || ! body ) {
return {
success: false ,
errors: [{ field: 'body' , message: 'Request body must be JSON object' }]
}
}
const data = body as any
// Validate payer
if ( ! data . payer || typeof data . payer !== 'string' ) {
errors . push ({ field: 'payer' , message: 'Payer public key is required' })
} else if ( ! BASE58_REGEX . test ( data . payer )) {
errors . push ({ field: 'payer' , message: 'Invalid base58 public key' })
}
// Validate accounts
if ( ! data . accounts || typeof data . accounts !== 'object' ) {
errors . push ({ field: 'accounts' , message: 'Accounts object is required' })
} else {
for ( const [ key , value ] of Object . entries ( data . accounts )) {
if ( typeof value !== 'string' || ! BASE58_REGEX . test ( value as string )) {
errors . push ({
field: `accounts. ${ key } ` ,
message: `Invalid public key for " ${ key } "`
})
}
}
}
// Validate args (must be object if present)
if ( data . args !== undefined && typeof data . args !== 'object' ) {
errors . push ({ field: 'args' , message: 'Args must be an object' })
}
if ( errors . length > 0 ) {
return { success: false , errors }
}
return {
success: true ,
data: {
payer: data . payer ,
accounts: data . accounts ,
args: data . args || {},
},
}
}
SQL Injection Prevention
Always use prepared statements:
// ✅ SAFE - Parameterized query
const result = await db
. prepare ( 'SELECT * FROM projects WHERE id = ?' )
. bind ( projectId )
. first ()
// ❌ UNSAFE - String concatenation
const result = await db
. prepare ( `SELECT * FROM projects WHERE id = ' ${ projectId } '` )
. first ()
XSS Prevention
Content-Type headers set correctly
No user input rendered directly in HTML
React automatically escapes JSX content
API returns JSON only (not HTML)
Rate Limiting
Distributed rate limiting using Cloudflare KV :
Implementation
export function rateLimiter ( opts : RateLimitOptions ) {
const { limit , windowSec , prefix = 'rl' } = opts
return async ( c : Context , next : Next ) => {
const cache = c . env . CACHE
const ip = c . req . header ( 'CF-Connecting-IP' ) || 'unknown'
const key = ` ${ prefix } : ${ ip } `
const raw = await cache . get ( key )
const now = Date . now ()
let entry : RateLimitEntry
if ( raw ) {
entry = JSON . parse ( raw )
if ( now > entry . resetAt ) {
entry = { count: 1 , resetAt: now + windowSec * 1000 }
} else {
entry . count ++
}
} else {
entry = { count: 1 , resetAt: now + windowSec * 1000 }
}
// Set headers
c . header ( 'X-RateLimit-Limit' , String ( limit ))
c . header ( 'X-RateLimit-Remaining' , String ( Math . max ( 0 , limit - entry . count )))
c . header ( 'X-RateLimit-Reset' , String ( Math . ceil ( entry . resetAt / 1000 )))
if ( entry . count > limit ) {
const retryAfter = Math . ceil (( entry . resetAt - now ) / 1000 )
c . header ( 'Retry-After' , String ( retryAfter ))
return c . json (
{
error: 'Too Many Requests' ,
message: `Rate limit exceeded. Try again in ${ retryAfter } s.` ,
retryAfter ,
},
429
)
}
await cache . put ( key , JSON . stringify ( entry ), { expirationTtl: windowSec + 10 })
await next ()
}
}
Rate Limit Presets
// General API endpoints
export const apiRateLimit = rateLimiter ({
limit: 100 , // 100 requests
windowSec: 60 , // per minute
prefix: 'rl:api' ,
})
// Authentication endpoints
export const authRateLimit = rateLimiter ({
limit: 20 ,
windowSec: 60 ,
prefix: 'rl:auth' ,
})
// IDL upload (more restrictive)
export const uploadRateLimit = rateLimiter ({
limit: 10 ,
windowSec: 60 ,
prefix: 'rl:upload' ,
})
// Transaction building
export const buildRateLimit = rateLimiter ({
limit: 30 ,
windowSec: 60 ,
prefix: 'rl:build' ,
})
Secret Management
Cloudflare Workers Secrets
Sensitive values stored as encrypted secrets :
# Set secrets (encrypted at rest)
wrangler secret put GITHUB_OAUTH_SECRET
wrangler secret put JWT_SECRET
wrangler secret put SOLANA_RPC_URL
Access in code:
const jwtSecret = c . env . JWT_SECRET // Available at runtime
Environment Variables
Non-sensitive config in wrangler.toml:
[ env . production . vars ]
ENVIRONMENT = "production"
FRONTEND_URL = "https://orquestra.dev"
API_BASE_URL = "https://api.orquestra.dev"
CORS_ORIGIN = "https://orquestra.dev"
Secret Rotation
Best Practices:
Rotate JWT secrets every 90 days
Rotate OAuth secrets on breach
Use different secrets per environment
Never commit secrets to git
Use wrangler secret for production
Authorization
Resource Ownership
Check user owns resource before modification:
app . delete ( '/api/idl/:projectId' , authMiddleware , async ( c ) => {
const userId = c . get ( 'userId' )
const projectId = c . req . param ( 'projectId' )
// Verify ownership
const project = await db
. prepare ( 'SELECT * FROM projects WHERE id = ? AND user_id = ?' )
. bind ( projectId , userId )
. first ()
if ( ! project ) {
return c . json ({ error: 'Project not found or access denied' }, 404 )
}
// Delete project
await db . prepare ( 'DELETE FROM projects WHERE id = ?' ). bind ( projectId ). run ()
return c . json ({ success: true })
})
Public vs Private Projects
// Public projects - no auth required
app . get ( '/api/:projectId/instructions' , async ( c ) => {
const projectId = c . req . param ( 'projectId' )
const project = await db
. prepare ( 'SELECT * FROM projects WHERE id = ? AND is_public = 1' )
. bind ( projectId )
. first ()
if ( ! project ) {
return c . json ({ error: 'Project not found or private' }, 404 )
}
// Return public data
})
// Private projects - require API key
app . get (
'/api/:projectId/instructions' ,
apiKeyMiddleware ,
async ( c ) => {
// Access granted via API key
}
)
middleware/security-headers.ts
export function securityHeaders ( c : Context , next : Next ) {
c . header ( 'X-Content-Type-Options' , 'nosniff' )
c . header ( 'X-Frame-Options' , 'DENY' )
c . header ( 'X-XSS-Protection' , '1; mode=block' )
c . header ( 'Referrer-Policy' , 'strict-origin-when-cross-origin' )
c . header (
'Content-Security-Policy' ,
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
)
await next ()
}
Monitoring & Auditing
Request Logging
middleware/request-logger.ts
export async function requestLogger ( c : Context , next : Next ) {
const start = Date . now ()
const method = c . req . method
const path = c . req . path
const ip = c . req . header ( 'CF-Connecting-IP' )
await next ()
const duration = Date . now () - start
const status = c . res . status
console . log ( JSON . stringify ({
timestamp: new Date (). toISOString (),
method ,
path ,
status ,
duration ,
ip ,
userAgent: c . req . header ( 'User-Agent' ),
}))
}
Error Tracking
try {
// Sensitive operation
} catch ( error ) {
console . error ( 'Error:' , {
message: error . message ,
stack: error . stack ,
context: { userId , projectId },
})
return c . json ({ error: 'Internal server error' }, 500 )
}
Security Best Practices
Use HTTPS only (enforced by Cloudflare)
Rotate JWT secrets regularly
Set token expiration (7 days max)
Invalidate tokens on logout
Use secure session storage
Check resource ownership
Implement least privilege
Validate all user inputs
Use prepared SQL statements
Separate public/private data
Implement per-IP limits
Limit per API key
Use exponential backoff
Return Retry-After header
Monitor abuse patterns
Never log secrets
Encrypt sensitive data at rest (D1)
Use TLS for all connections
Sanitize error messages
Implement CORS properly
Security Checklist
Reporting Security Issues
Do not create public GitHub issues for security vulnerabilities.
Report security issues via:
Backend Architecture Middleware and service layer implementation
System Architecture Overall system design and data flow
API Reference API endpoints and authentication methods
Deployment Production deployment and configuration