PRIV ProtocolPRIV Docs
Security

CORS Configuration

Cross-Origin Resource Sharing policies for PRIV API endpoints.

Overview

CORS (Cross-Origin Resource Sharing) controls which domains can make requests to PRIV APIs. We use a tiered CORS policy based on endpoint sensitivity.

Public SDK Endpoints

Open CORS for SDK event tracking from any website.

Authenticated Endpoints

Restricted CORS for sensitive operations with credentials.


CORS Tiers

PRIV implements three CORS tiers based on endpoint sensitivity:

Tier 1: Public SDK Endpoints

These endpoints accept requests from any origin to support the analytics SDK:

Endpoint PatternCORS Policy
/api/v1/analytics/*Access-Control-Allow-Origin: *
/api/v1/events/*Access-Control-Allow-Origin: *
/api/v1/identify/*Access-Control-Allow-Origin: *
// These endpoints work from any website
await fetch('https://api.priv.io/v1/events', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer pk_live_xxxxx', // Publishable key
  },
  body: JSON.stringify({ events: [...] }),
})

Security notes:

  • No credentials (Access-Control-Allow-Credentials: false)
  • Rate limited per API key
  • Only publishable keys accepted
  • Limited to read/write events only

Tier 2: Authenticated Endpoints

Sensitive endpoints require credentials and have restricted origins:

Endpoint PatternCORS Policy
/api/v1/user/*Restricted origins + credentials
/api/v1/billing/*Restricted origins + credentials
/api/v1/marketplace/*Restricted origins + credentials
/api/v1/admin/*Restricted origins + credentials
// Must be called from allowed origins
await fetch('https://api.priv.io/v1/user/profile', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer sk_live_xxxxx', // Secret key
  },
  credentials: 'include', // Send cookies
})

Security notes:

  • Access-Control-Allow-Credentials: true
  • Specific Access-Control-Allow-Origin (not *)
  • HttpOnly cookies supported
  • Secret keys required

Tier 3: Internal Endpoints

Some endpoints are not exposed via CORS:

Endpoint PatternAccess
/api/internal/*Server-to-server only
/api/admin/*Admin dashboard only
/api/webhooks/*Webhook callbacks only

Configuration

Setting Allowed Origins

Configure allowed origins for your application:

# .env
ALLOWED_ORIGINS=https://app.priv.io,https://priv.io,https://staging.priv.io

Next.js Implementation

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') || []

// Public SDK endpoints - allow all origins
const PUBLIC_ENDPOINTS = [
  '/api/v1/analytics',
  '/api/v1/events',
  '/api/v1/identify',
]

// Endpoints that require credentials
const AUTHENTICATED_ENDPOINTS = [
  '/api/v1/user',
  '/api/v1/billing',
  '/api/v1/marketplace',
]

export function middleware(request: NextRequest) {
  const origin = request.headers.get('origin')
  const pathname = request.nextUrl.pathname

  // Handle preflight requests
  if (request.method === 'OPTIONS') {
    return handlePreflight(request, origin, pathname)
  }

  // Add CORS headers to response
  const response = NextResponse.next()
  addCorsHeaders(response, origin, pathname)

  return response
}

function handlePreflight(
  request: NextRequest,
  origin: string | null,
  pathname: string
): NextResponse {
  const response = new NextResponse(null, { status: 204 })

  if (isPublicEndpoint(pathname)) {
    // Public endpoints - allow all origins
    response.headers.set('Access-Control-Allow-Origin', '*')
    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
    response.headers.set(
      'Access-Control-Allow-Headers',
      'Content-Type, Authorization'
    )
    response.headers.set('Access-Control-Max-Age', '86400')
  } else if (isAuthenticatedEndpoint(pathname) && isAllowedOrigin(origin)) {
    // Authenticated endpoints - restricted origins
    response.headers.set('Access-Control-Allow-Origin', origin!)
    response.headers.set(
      'Access-Control-Allow-Methods',
      'GET, POST, PUT, DELETE, OPTIONS'
    )
    response.headers.set(
      'Access-Control-Allow-Headers',
      'Content-Type, Authorization'
    )
    response.headers.set('Access-Control-Allow-Credentials', 'true')
    response.headers.set('Access-Control-Max-Age', '86400')
  }

  return response
}

function addCorsHeaders(
  response: NextResponse,
  origin: string | null,
  pathname: string
): void {
  if (isPublicEndpoint(pathname)) {
    response.headers.set('Access-Control-Allow-Origin', '*')
  } else if (isAuthenticatedEndpoint(pathname) && isAllowedOrigin(origin)) {
    response.headers.set('Access-Control-Allow-Origin', origin!)
    response.headers.set('Access-Control-Allow-Credentials', 'true')
  }
}

function isPublicEndpoint(pathname: string): boolean {
  return PUBLIC_ENDPOINTS.some(ep => pathname.startsWith(ep))
}

function isAuthenticatedEndpoint(pathname: string): boolean {
  return AUTHENTICATED_ENDPOINTS.some(ep => pathname.startsWith(ep))
}

function isAllowedOrigin(origin: string | null): boolean {
  if (!origin) return false
  return ALLOWED_ORIGINS.includes(origin)
}

export const config = {
  matcher: '/api/:path*',
}

CORS Headers Reference

Request Headers

HeaderDescription
OriginThe origin making the request
Access-Control-Request-MethodPreflight: requested method
Access-Control-Request-HeadersPreflight: requested headers

Response Headers

HeaderDescription
Access-Control-Allow-OriginAllowed origin(s)
Access-Control-Allow-MethodsAllowed HTTP methods
Access-Control-Allow-HeadersAllowed request headers
Access-Control-Allow-CredentialsWhether to include credentials
Access-Control-Expose-HeadersHeaders accessible to JavaScript
Access-Control-Max-AgePreflight cache duration

Common Patterns

Pattern 1: SDK Integration (Public)

// Works from any website
const response = await fetch('https://api.priv.io/v1/events', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer pk_live_xxxxx',
  },
  // NO credentials - this is important
  body: JSON.stringify({
    events: [{ type: 'page_view', timestamp: new Date().toISOString() }],
  }),
})

Pattern 2: Dashboard API (Authenticated)

// Only works from allowed origins
const response = await fetch('https://api.priv.io/v1/user/profile', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer sk_live_xxxxx',
  },
  credentials: 'include', // Include cookies for session
})

Pattern 3: Server-to-Server (No CORS)

// Server-side only - CORS doesn't apply
// Use from API routes, Edge Functions, etc.
const response = await fetch('https://api.priv.io/v1/internal/batch', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.PRIV_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ operations: [...] }),
})

Security Considerations

Never Combine * with Credentials

// DANGEROUS - Never do this!
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Credentials', 'true')

// This is actually blocked by browsers, but the intention is still wrong

Validate Origin Header

// Don't trust the Origin header blindly
function validateOrigin(origin: string | null): boolean {
  if (!origin) return false

  // Check against whitelist
  if (!ALLOWED_ORIGINS.includes(origin)) {
    return false
  }

  // Additional validation: must be HTTPS in production
  if (process.env.NODE_ENV === 'production') {
    try {
      const url = new URL(origin)
      if (url.protocol !== 'https:') {
        return false
      }
    } catch {
      return false
    }
  }

  return true
}

Limit Exposed Headers

// Only expose headers that are necessary
response.headers.set(
  'Access-Control-Expose-Headers',
  'X-Request-Id, X-RateLimit-Remaining'
)

// Don't expose sensitive headers like:
// - X-Internal-User-Id
// - X-Debug-Info
// - etc.

Troubleshooting

Common CORS Errors

Error: "No 'Access-Control-Allow-Origin' header"

Access to fetch at 'https://api.priv.io/v1/events' from origin
'https://example.com' has been blocked by CORS policy

Causes:

  • Origin not in allowed list
  • Preflight request failed
  • Server error before CORS headers added

Solutions:

  1. Add your domain to allowed origins in dashboard
  2. Check for server errors in API logs
  3. Verify preflight response has correct headers

Error: "Credentials not supported for wildcard origin"

The value of the 'Access-Control-Allow-Origin' header must not be
the wildcard '*' when the request's credentials mode is 'include'

Cause: Trying to send credentials to a public endpoint

Solution: Remove credentials: 'include' from SDK requests:

// Wrong
fetch('https://api.priv.io/v1/events', {
  credentials: 'include', // Remove this
})

// Correct
fetch('https://api.priv.io/v1/events', {
  // No credentials for public endpoints
})

Error: "Header not allowed by Access-Control-Allow-Headers"

Request header field X-Custom-Header is not allowed by
Access-Control-Allow-Headers

Cause: Sending a header not in the allowed list

Solution: Only send allowed headers, or add custom header to allowlist:

// Standard allowed headers
const ALLOWED_HEADERS = [
  'Content-Type',
  'Authorization',
  'X-Request-Id',
]

response.headers.set(
  'Access-Control-Allow-Headers',
  ALLOWED_HEADERS.join(', ')
)

Testing CORS

Browser DevTools

  1. Open DevTools (F12)
  2. Go to Network tab
  3. Make a request
  4. Check Response Headers for CORS headers

curl Testing

# Test preflight request
curl -X OPTIONS https://api.priv.io/v1/events \
  -H "Origin: https://example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization" \
  -v

# Test actual request
curl -X POST https://api.priv.io/v1/events \
  -H "Origin: https://example.com" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer pk_live_xxxxx" \
  -d '{"events":[]}' \
  -v

Automated Testing

import { describe, it, expect } from 'vitest'

describe('CORS Configuration', () => {
  it('should allow any origin for public endpoints', async () => {
    const response = await fetch('https://api.priv.io/v1/events', {
      method: 'OPTIONS',
      headers: {
        'Origin': 'https://random-site.com',
        'Access-Control-Request-Method': 'POST',
      },
    })

    expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
  })

  it('should restrict authenticated endpoints', async () => {
    const response = await fetch('https://api.priv.io/v1/user/profile', {
      method: 'OPTIONS',
      headers: {
        'Origin': 'https://unauthorized-site.com',
        'Access-Control-Request-Method': 'GET',
      },
    })

    expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull()
  })

  it('should allow whitelisted origins for authenticated endpoints', async () => {
    const response = await fetch('https://api.priv.io/v1/user/profile', {
      method: 'OPTIONS',
      headers: {
        'Origin': 'https://app.priv.io',
        'Access-Control-Request-Method': 'GET',
      },
    })

    expect(response.headers.get('Access-Control-Allow-Origin')).toBe(
      'https://app.priv.io'
    )
    expect(response.headers.get('Access-Control-Allow-Credentials')).toBe(
      'true'
    )
  })
})

Resources