Skip to main content

Overview

Orquestra uses JWT (JSON Web Token) for stateless authentication. After logging in via GitHub OAuth, you receive a JWT that must be included in subsequent API requests.

Token Format

JWTs are composed of three parts separated by dots:
header.payload.signature
Example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYmMxMjNkZWY0NTYiLCJ1c2VybmFtZSI6Im9jdG9jYXQiLCJpYXQiOjE3MDUzMTQwMDAsImV4cCI6MTcwNTkxODgwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
{
  "alg": "HS256",
  "typ": "JWT"
}
  • Algorithm: HMAC-SHA256 (symmetric signing)
  • Type: JWT

Payload (Claims)

{
  "sub": "abc123def456",
  "username": "octocat",
  "iat": 1705314000,
  "exp": 1705918800
}
sub
string
Subject - the user ID
username
string
GitHub username
iat
number
Issued At - Unix timestamp when the token was created
exp
number
Expiration - Unix timestamp when the token expires (7 days from issue)

Signature

The signature is computed using HMAC-SHA256:
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)
The secret is stored securely in the JWT_SECRET environment variable.

Using JWT Tokens

Authorization Header

Include the JWT in the Authorization header with the Bearer scheme:
Authorization: Bearer <your_jwt_token>

Example Requests

curl https://api.orquestra.so/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Token Expiration

Default Expiration

JWT tokens expire 7 days after issuance.

Checking Expiration

You can decode the JWT client-side to check expiration:
function isTokenExpired(token) {
  try {
    const [, payload] = token.split('.');
    const decoded = JSON.parse(atob(payload));
    return decoded.exp * 1000 < Date.now();
  } catch {
    return true;
  }
}

const token = localStorage.getItem('authToken');
if (isTokenExpired(token)) {
  // Redirect to login
  window.location.href = '/login';
}

Handling Expired Tokens

When a token expires, the API returns a 401 Unauthorized error:
{
  "error": "Unauthorized",
  "message": "Invalid or expired token"
}
Your application should:
  1. Detect the 401 error
  2. Clear the stored token
  3. Redirect the user to the login page
async function apiRequest(url, options = {}) {
  const token = localStorage.getItem('authToken');
  
  const response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${token}`
    }
  });
  
  if (response.status === 401) {
    // Token expired or invalid
    localStorage.removeItem('authToken');
    window.location.href = '/login';
    return;
  }
  
  return response.json();
}

Token Refresh

Orquestra does not currently support token refresh. When a token expires, the user must log in again via GitHub OAuth.
To minimize user disruption:
  • Tokens have a 7-day lifespan
  • Users can stay logged in for a week of continuous use
  • Consider warning users when their token is about to expire (e.g., within 24 hours)

Security Best Practices

  • Frontend: Use localStorage or sessionStorage (not cookies if you need cross-domain access)
  • Mobile: Use secure storage (Keychain on iOS, Keystore on Android)
  • CLI: Store in a secure file with restricted permissions (chmod 600)
  • Never commit tokens to version control
  • Sanitize all user input
  • Use Content Security Policy (CSP) headers
  • Avoid eval() and inline scripts
  • If using cookies, set HttpOnly and Secure flags
Always use HTTPS to prevent token interception. Never send tokens over HTTP.
  • Always validate the signature
  • Check expiration time
  • Verify the issuer (if applicable)
  • Orquestra handles this automatically for all protected endpoints
If a token is compromised:
  1. User should log in again to get a new token
  2. Old token will expire after 7 days
  3. Consider implementing a token blocklist for immediate revocation (not currently supported)

Authentication Middleware

Orquestra uses two middleware functions for JWT authentication:

Required Authentication

Endpoints protected by authMiddleware require a valid JWT:
  • /auth/me - Get current user
  • /api/projects/mine - List user’s projects
  • /api/projects/:projectId - Update/delete projects
  • /api/projects/:projectId/keys - Manage API keys
  • All owner-only endpoints
If the token is missing or invalid, returns 401 Unauthorized.

Optional Authentication

Endpoints using optionalAuthMiddleware work with or without a token:
  • /api/projects - List projects (shows private projects if authenticated)
  • /api/:projectId/docs - Get documentation (shows private docs if owner)
  • /api/:projectId/addresses - List known addresses
If a token is provided and valid, user context is set. If not, the request continues anonymously.

Error Responses

401 Unauthorized
Missing Authorization Header
{
  "error": "Unauthorized",
  "message": "Missing or invalid Authorization header"
}
401 Unauthorized
Invalid Token Format
{
  "error": "Unauthorized",
  "message": "Invalid or expired token"
}
401 Unauthorized
Expired Token
{
  "error": "Unauthorized",
  "message": "Invalid or expired token"
}
401 Unauthorized
Invalid Signature
{
  "error": "Unauthorized",
  "message": "Invalid or expired token"
}

Implementation Reference

The JWT service is implemented in /packages/worker/src/services/jwt.ts using the Web Crypto API (compatible with Cloudflare Workers).

Token Generation

export async function generateJWT(
  payload: { sub: string; username: string },
  secret: string,
  expiresInSeconds: number = 7 * 24 * 60 * 60
): Promise<string>

Token Verification

export async function verifyJWT(
  token: string,
  secret: string
): Promise<JWTPayload>
The implementation uses HMAC-SHA256 with Base64URL encoding, following the JWT standard (RFC 7519).

Comparison with API Keys

FeatureJWTAPI Key
Use CaseUser authenticationProgrammatic access
Lifetime7 daysConfigurable or permanent
ScopeFull user accessProject-specific
RevocationCannot revoke (expires naturally)Can delete anytime
HeaderAuthorization: Bearer <token>X-API-Key: <key>
Best ForWeb/mobile appsCI/CD, bots, integrations
See API Keys for programmatic authentication.

Build docs developers (and LLMs) love