Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Multi-Site Decap CMS OAuth Provider

Initial Setup

This section converts your existing single-site OAuth provider to support multiple sites.

Step 1: Backup Current Configuration

Before making changes, backup your working setup:

cd /opt/docker/decap-oauth
cp server.js server.js.backup
cp docker-compose.yml docker-compose.yml.backup

If something goes wrong, you can restore:

cp server.js.backup server.js
cp docker-compose.yml.backup docker-compose.yml

Step 2: Update server.js for Multi-Site Support

Replace the contents of server.js:

nano server.js

New multi-site server.js:

const express = require('express');
const cors = require('cors');
const { AuthorizationCode } = require('simple-oauth2');

const app = express();
const port = process.env.PORT || 3000;

// CORS configuration - allow all origins
app.use(cors({
  origin: true,
  credentials: true
}));

// Parse query parameters
app.use(express.urlencoded({ extended: true }));

// ============================================
// CONFIGURATION: Add your sites here
// ============================================

const clients = {
  // Site 1: Boeygembos (example)
  boeygembos: {
    client: new AuthorizationCode({
      client: {
        id: process.env.BOEYGEMBOS_CLIENT_ID,
        secret: process.env.BOEYGEMBOS_CLIENT_SECRET,
      },
      auth: {
        tokenHost: 'https://github.com',
        tokenPath: '/login/oauth/access_token',
        authorizePath: '/login/oauth/authorize',
      },
    }),
    allowedOrigins: ['https://www.boeygembos.be'],
    description: 'Boeygembos Forest Information Site'
  },
  
  // Site 2: Add your next site here (template)
  // site2: {
  //   client: new AuthorizationCode({
  //     client: {
  //       id: process.env.SITE2_CLIENT_ID,
  //       secret: process.env.SITE2_CLIENT_SECRET,
  //     },
  //     auth: {
  //       tokenHost: 'https://github.com',
  //       tokenPath: '/login/oauth/access_token',
  //       authorizePath: '/login/oauth/authorize',
  //     },
  //   }),
  //   allowedOrigins: ['https://www.site2.com'],
  //   description: 'Site 2 Description'
  // },
};

// ============================================
// Helper Functions
// ============================================

// Get client configuration by site name
function getClientBySite(siteName) {
  return clients[siteName];
}

// Validate origin against allowed origins
function isOriginAllowed(siteName, origin) {
  const clientConfig = getClientBySite(siteName);
  if (!clientConfig) return false;
  return clientConfig.allowedOrigins.includes(origin);
}

// ============================================
// Routes
// ============================================

// Root endpoint - shows configuration
app.get('/', (req, res) => {
  const sites = Object.keys(clients).map(siteName => ({
    name: siteName,
    description: clients[siteName].description,
    allowedOrigins: clients[siteName].allowedOrigins,
    endpoints: {
      auth: `/auth/${siteName}`,
      callback: `/callback/${siteName}`
    }
  }));
  
  res.json({
    message: 'Multi-Site OAuth Provider for Decap CMS',
    version: '2.0',
    configured_sites: sites.length,
    sites: sites,
    health_check: '/health'
  });
});

// Health check endpoint
app.get('/health', (req, res) => {
  const siteCount = Object.keys(clients).length;
  res.json({
    status: 'healthy',
    message: 'OAuth provider is running',
    configured_sites: siteCount,
    uptime: process.uptime()
  });
});

// Auth endpoint - redirects to GitHub OAuth
app.get('/auth/:site', (req, res) => {
  const siteName = req.params.site;
  const clientConfig = getClientBySite(siteName);
  
  if (!clientConfig) {
    console.error(`Unknown site requested: ${siteName}`);
    return res.status(400).json({
      error: 'Unknown site',
      message: `Site '${siteName}' is not configured`,
      configured_sites: Object.keys(clients)
    });
  }

  const authorizationUri = clientConfig.client.authorizeURL({
    redirect_uri: `${process.env.BASE_URL}/callback/${siteName}`,
    scope: 'repo,user',
    state: req.query.state || '',
  });
  
  console.log(`Auth request for site: ${siteName}`);
  res.redirect(authorizationUri);
});

// Callback endpoint - handles GitHub OAuth callback
app.get('/callback/:site', async (req, res) => {
  const { code, state } = req.query;
  const siteName = req.params.site;
  const clientConfig = getClientBySite(siteName);
  
  if (!clientConfig) {
    console.error(`Unknown site in callback: ${siteName}`);
    return res.status(400).json({
      error: 'Unknown site',
      message: `Site '${siteName}' is not configured`
    });
  }

  try {
    const tokenParams = {
      code,
      redirect_uri: `${process.env.BASE_URL}/callback/${siteName}`,
    };
    
    const accessToken = await clientConfig.client.getToken(tokenParams);
    const token = accessToken.token.access_token;
    
    // Get the origin from allowed origins
    const redirectOrigin = clientConfig.allowedOrigins[0];
    
    console.log(`Successfully authenticated for site: ${siteName}`);
    
    // Send token back to CMS via postMessage
    const script = `
      <!DOCTYPE html>
      <html>
      <head>
        <title>Authentication Successful</title>
        <style>
          body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
          }
          .message {
            text-align: center;
            padding: 2rem;
            background: rgba(255,255,255,0.1);
            border-radius: 10px;
            backdrop-filter: blur(10px);
          }
        </style>
      </head>
      <body>
        <div class="message">
          <h1>✓ Authentication Successful</h1>
          <p>You can close this window.</p>
        </div>
        <script>
          (function() {
            function receiveMessage(e) {
              console.log("receiveMessage %o", e);
              // Send success message to opener
              window.opener.postMessage(
                'authorization:github:success:${JSON.stringify({ token, provider: 'github' })}',
                e.origin
              );
            }
            window.addEventListener("message", receiveMessage, false);
            // Notify opener that we're authorizing
            window.opener.postMessage("authorizing:github", "${redirectOrigin}");
            
            // Auto-close after 2 seconds
            setTimeout(function() {
              window.close();
            }, 2000);
          })();
        </script>
      </body>
      </html>
    `;
    res.send(script);
  } catch (error) {
    console.error(`Authentication failed for site ${siteName}:`, error.message);
    res.status(500).send(`
      <!DOCTYPE html>
      <html>
      <head>
        <title>Authentication Failed</title>
        <style>
          body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            color: white;
          }
          .message {
            text-align: center;
            padding: 2rem;
            background: rgba(255,255,255,0.1);
            border-radius: 10px;
            backdrop-filter: blur(10px);
          }
          .error {
            background: rgba(0,0,0,0.2);
            padding: 1rem;
            border-radius: 5px;
            margin-top: 1rem;
            font-family: monospace;
            font-size: 0.9em;
          }
        </style>
      </head>
      <body>
        <div class="message">
          <h1>✗ Authentication Failed</h1>
          <p>There was an error during authentication.</p>
          <div class="error">${error.message}</div>
          <p style="margin-top: 1rem; font-size: 0.9em;">You can close this window and try again.</p>
        </div>
      </body>
      </html>
    `);
  }
});

// ============================================
// Start Server
// ============================================

app.listen(port, () => {
  const siteNames = Object.keys(clients);
  console.log('='.repeat(50));
  console.log('Multi-Site OAuth Provider for Decap CMS');
  console.log('='.repeat(50));
  console.log(`Server listening on port ${port}`);
  console.log(`Base URL: ${process.env.BASE_URL}`);
  console.log(`Configured sites: ${siteNames.length}`);
  console.log('');
  console.log('Sites:');
  siteNames.forEach(siteName => {
    const config = clients[siteName];
    console.log(`  - ${siteName}`);
    console.log(`    Description: ${config.description}`);
    console.log(`    Allowed origins: ${config.allowedOrigins.join(', ')}`);
    console.log(`    Auth endpoint: ${process.env.BASE_URL}/auth/${siteName}`);
  });
  console.log('='.repeat(50));
});

Save with Ctrl+X, Y, Enter.

Key changes from single-site version:

  • clients object holds multiple site configurations
  • Routes include :site parameter (e.g., /auth/boeygembos)
  • Each site has its own credentials and allowed origins
  • Root endpoint (/) shows all configured sites
  • Better error handling and logging

Step 3: Update docker-compose.yml

nano docker-compose.yml

New multi-site docker-compose.yml:

version: '3.8'

services:
  decap-oauth:
    build: .
    container_name: decap-oauth
    restart: unless-stopped
    networks:
      - web
    environment:
      # Server configuration
      - NODE_ENV=production
      - PORT=3000
      - BASE_URL=https://oauth.boeygembos.be
      
      # Site 1: Boeygembos
      - BOEYGEMBOS_CLIENT_ID=your_boeygembos_client_id_here
      - BOEYGEMBOS_CLIENT_SECRET=your_boeygembos_client_secret_here
      
      # Site 2: Add credentials for next site
      # - SITE2_CLIENT_ID=xxx
      # - SITE2_CLIENT_SECRET=xxx
      
      # Site 3: Add credentials for another site
      # - SITE3_CLIENT_ID=xxx
      # - SITE3_CLIENT_SECRET=xxx
      
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.decap-oauth.rule=Host(`oauth.boeygembos.be`)"
      - "traefik.http.routers.decap-oauth.entrypoints=websecure"
      - "traefik.http.routers.decap-oauth.tls.certresolver=letsencrypt"
      - "traefik.http.services.decap-oauth.loadbalancer.server.port=3000"

networks:
  web:
    external: true

IMPORTANT: Replace your_boeygembos_client_id_here and your_boeygembos_client_secret_here with your actual GitHub OAuth App credentials.

Save with Ctrl+X, Y, Enter.


Step 4: Update Existing Site’s Decap CMS Config

Your existing site (Boeygembos) needs a small config update:

File: boeygembos-app/public/admin/config.yml

Change:

backend:
  name: github
  repo: boeygembos/boeygembos.github.io
  branch: release
  base_url: https://oauth.boeygembos.be
  auth_endpoint: auth  # Old way

To:

backend:
  name: github
  repo: boeygembos/boeygembos.github.io
  branch: release
  base_url: https://oauth.boeygembos.be
  auth_endpoint: auth/boeygembos  # New way - includes site name

Note: The only change is adding /boeygembos to the auth_endpoint.

Commit and push:

git add public/admin/config.yml
git commit -m "Update OAuth endpoint for multi-site support"
git push origin release

Step 5: Rebuild and Deploy

cd /opt/docker/decap-oauth

# Rebuild container with new code
docker compose down
docker compose build --no-cache
docker compose up -d

# Watch logs
docker compose logs -f

You should see:

==================================================
Multi-Site OAuth Provider for Decap CMS
==================================================
Server listening on port 3000
Base URL: https://oauth.boeygembos.be
Configured sites: 1

Sites:
  - boeygembos
    Description: Boeygembos Forest Information Site
    Allowed origins: https://www.boeygembos.be
    Auth endpoint: https://oauth.boeygembos.be/auth/boeygembos
==================================================

Press Ctrl+C to exit logs.


Step 6: Test Existing Site

  1. Wait ~2 minutes for GitHub Actions to deploy the config change
  2. Go to: https://www.boeygembos.be/admin/
  3. Click “Login with GitHub”
  4. Should work exactly as before ✅

If it doesn’t work, check Troubleshooting section.


Step 7: Verify Multi-Site Setup

Check the provider info endpoint:

curl https://oauth.boeygembos.be/ | jq

Should return:

{
  "message": "Multi-Site OAuth Provider for Decap CMS",
  "version": "2.0",
  "configured_sites": 1,
  "sites": [
    {
      "name": "boeygembos",
      "description": "Boeygembos Forest Information Site",
      "allowedOrigins": ["https://www.boeygembos.be"],
      "endpoints": {
        "auth": "/auth/boeygembos",
        "callback": "/callback/boeygembos"
      }
    }
  ],
  "health_check": "/health"
}

Adding Additional Sites

Now that you have multi-site support, adding new sites is straightforward.

Overview: 5-Step Process

For each new site:

  1. Create GitHub OAuth App
  2. Add site to server.js
  3. Add credentials to docker-compose.yml
  4. Restart container
  5. Configure site’s Decap CMS

Time per site: ~15 minutes


Example: Adding “Client Portfolio” Site

Let’s walk through adding a second site: www.clientportfolio.com

Step 1: Create GitHub OAuth App

  1. Go to: https://github.com/settings/developers
  2. Click New OAuth App
  3. Fill in:
    • Application name: Client Portfolio CMS
    • Homepage URL: https://www.clientportfolio.com
    • Authorization callback URL: https://oauth.boeygembos.be/callback/clientportfolio
      • ⚠️ Important: Use site’s short name in path (no spaces, lowercase)
  4. Click Register application
  5. Copy Client ID
  6. Click Generate a new client secret
  7. Copy Client Secret

Save these somewhere secure!

Step 2: Add Site to server.js

SSH to VPS and edit server.js:

cd /opt/docker/decap-oauth
nano server.js

Find the clients object and add your new site:

const clients = {
  // Existing site
  boeygembos: {
    client: new AuthorizationCode({
      client: {
        id: process.env.BOEYGEMBOS_CLIENT_ID,
        secret: process.env.BOEYGEMBOS_CLIENT_SECRET,
      },
      auth: {
        tokenHost: 'https://github.com',
        tokenPath: '/login/oauth/access_token',
        authorizePath: '/login/oauth/authorize',
      },
    }),
    allowedOrigins: ['https://www.boeygembos.be'],
    description: 'Boeygembos Forest Information Site'
  },
  
  // NEW SITE - Add this
  clientportfolio: {
    client: new AuthorizationCode({
      client: {
        id: process.env.CLIENTPORTFOLIO_CLIENT_ID,
        secret: process.env.CLIENTPORTFOLIO_CLIENT_SECRET,
      },
      auth: {
        tokenHost: 'https://github.com',
        tokenPath: '/login/oauth/access_token',
        authorizePath: '/login/oauth/authorize',
      },
    }),
    allowedOrigins: ['https://www.clientportfolio.com'],
    description: 'Client Portfolio Website'
  },
};

Naming conventions:

  • Object key: clientportfolio (lowercase, no spaces - used in URL paths)
  • Environment variables: CLIENTPORTFOLIO_CLIENT_ID (uppercase with underscores)
  • Description: Human-readable name

Save with Ctrl+X, Y, Enter.

Step 3: Add Credentials to docker-compose.yml

nano docker-compose.yml

Add environment variables:

environment:
  # Server configuration
  - NODE_ENV=production
  - PORT=3000
  - BASE_URL=https://oauth.boeygembos.be
  
  # Site 1: Boeygembos
  - BOEYGEMBOS_CLIENT_ID=xxx
  - BOEYGEMBOS_CLIENT_SECRET=xxx
  
  # Site 2: Client Portfolio - ADD THESE
  - CLIENTPORTFOLIO_CLIENT_ID=paste_client_id_here
  - CLIENTPORTFOLIO_CLIENT_SECRET=paste_client_secret_here

Replace paste_client_id_here and paste_client_secret_here with actual values from Step 1.

Save with Ctrl+X, Y, Enter.

Step 4: Rebuild and Restart Container

docker compose down
docker compose build --no-cache
docker compose up -d

# Check logs - should show 2 configured sites
docker compose logs --tail=30

Verify it’s configured:

curl https://oauth.boeygembos.be/ | jq

Should show 2 sites now.

Step 5: Configure New Site’s Decap CMS

In your Client Portfolio repository, edit the Decap CMS config:

File: public/admin/config.yml (or wherever your config is)

backend:
  name: github
  repo: username/clientportfolio-repo  # Your actual repo
  branch: main  # Your branch
  base_url: https://oauth.boeygembos.be
  auth_endpoint: auth/clientportfolio  # Match the key from server.js

media_folder: "public/images"
public_folder: "/images"

collections:
  # Your collections here

Key points:

  • base_url points to the shared OAuth provider
  • auth_endpoint includes the site name: auth/clientportfolio
  • Site name must match the key in server.js clients object

Commit, push, and deploy the site.

Step 6: Test New Site

  1. Go to: https://www.clientportfolio.com/admin/
  2. Click “Login with GitHub”
  3. Should redirect to GitHub
  4. Authorize
  5. Should be logged in ✅

Quick Add Template

For future sites, use this template:

1. GitHub OAuth App:

  • Homepage: https://www.YOURSITE.com
  • Callback: https://oauth.boeygembos.be/callback/SITENAME

2. server.js addition:

  SITENAME: {
    client: new AuthorizationCode({
      client: {
        id: process.env.SITENAME_CLIENT_ID,
        secret: process.env.SITENAME_CLIENT_SECRET,
      },
      auth: {
        tokenHost: 'https://github.com',
        tokenPath: '/login/oauth/access_token',
        authorizePath: '/login/oauth/authorize',
      },
    }),
    allowedOrigins: ['https://www.YOURSITE.com'],
    description: 'SITE DESCRIPTION'
  },

3. docker-compose.yml addition:

  - SITENAME_CLIENT_ID=xxx
  - SITENAME_CLIENT_SECRET=xxx

4. Site’s config.yml:

backend:
  base_url: https://oauth.boeygembos.be
  auth_endpoint: auth/SITENAME

5. Restart:

docker compose down && docker compose build --no-cache && docker compose up -d

Configuration Reference

Environment Variable Naming

Pattern: SITENAME_CREDENTIAL_TYPE

Where:

  • SITENAME = Uppercase version of site key
  • CREDENTIAL_TYPE = CLIENT_ID or CLIENT_SECRET

Examples:

BOEYGEMBOS_CLIENT_ID
BOEYGEMBOS_CLIENT_SECRET
CLIENTSITE_CLIENT_ID
CLIENTSITE_CLIENT_SECRET
MYPORTFOLIO_CLIENT_ID
MYPORTFOLIO_CLIENT_SECRET

Site Key Naming Conventions

Good site keys:

  • boeygembos ✅
  • clientsite ✅
  • portfolio2024 ✅
  • myblog ✅

Bad site keys:

  • Client Site ❌ (spaces)
  • my-blog ❌ (hyphens cause issues)
  • Site#2 ❌ (special characters)
  • 2ndsite ❌ (starts with number)

Rules:

  • Lowercase only
  • No spaces
  • No special characters
  • Use letters and numbers only
  • Can’t start with a number

Callback URL Format

Pattern: https://oauth.boeygembos.be/callback/SITENAME

Where SITENAME matches the key in server.js.

Examples:

https://oauth.boeygembos.be/callback/boeygembos
https://oauth.boeygembos.be/callback/clientsite
https://oauth.boeygembos.be/callback/portfolio2024

Decap CMS Config Format

Minimal required config:

backend:
  name: github
  repo: username/repository
  branch: main
  base_url: https://oauth.boeygembos.be
  auth_endpoint: auth/SITENAME

With additional options:

backend:
  name: github
  repo: username/repository
  branch: main
  base_url: https://oauth.boeygembos.be
  auth_endpoint: auth/SITENAME
  open_authoring: true  # Optional: enable open authoring
  squash_merges: true   # Optional: squash commits

Troubleshooting

Issue 1: “Unknown site” Error

Symptoms:

  • Clicking “Login with GitHub” shows error page
  • Error message: “Site ‘xyz’ is not configured”

Causes:

  • Site key mismatch between server.js and auth_endpoint
  • Typo in site name
  • Forgot to restart container after adding site

Solutions:

  1. Check site key matches:
// In server.js
const clients = {
  mysite: { ... }  // Key is 'mysite'
};
# In site's config.yml
auth_endpoint: auth/mysite  # Must match exactly
  1. Verify container has latest code:
cd /opt/docker/decap-oauth
docker compose down
docker compose build --no-cache
docker compose up -d
docker compose logs | grep "Configured sites"
  1. Check configured sites:
curl https://oauth.boeygembos.be/ | jq '.sites[].name'

Issue 2: Authentication Fails with 401

Symptoms:

  • Redirects to GitHub successfully
  • Authorization fails with error
  • Console shows 401 or “Bad credentials”

Causes:

  • Wrong Client ID or Secret in docker-compose.yml
  • Client Secret was regenerated on GitHub
  • Environment variables not loaded

Solutions:

  1. Verify credentials in GitHub:
  • Go to: https://github.com/settings/developers
  • Find your OAuth App
  • Check Client ID matches
  • If needed, regenerate Client Secret
  1. Update docker-compose.yml:
nano docker-compose.yml
# Update the credentials
docker compose down
docker compose up -d
  1. Check environment variables loaded:
docker exec decap-oauth env | grep CLIENT

Should show all your CLIENT_ID and CLIENT_SECRET variables.

Issue 3: CORS Error in Browser Console

Symptoms:

  • Browser console shows CORS error
  • “Access-Control-Allow-Origin” error
  • Authentication popup closes immediately

Causes:

  • Site origin not in allowedOrigins array
  • HTTP vs HTTPS mismatch
  • Wrong domain in allowedOrigins

Solutions:

  1. Check allowedOrigins in server.js:
allowedOrigins: ['https://www.yoursite.com']  // Must match exactly

Must match:

  • Protocol (http vs https)
  • Subdomain (www vs non-www)
  • Domain spelling
  1. For development (testing locally):

Add localhost to allowedOrigins temporarily:

allowedOrigins: [
  'https://www.yoursite.com',
  'http://localhost:3000'  // For local testing
]

Remember to remove localhost in production!

Issue 4: Container Won’t Start

Symptoms:

  • docker compose up -d fails
  • Container keeps restarting
  • Can’t access OAuth provider

Causes:

  • Syntax error in server.js
  • Missing dependencies
  • Port conflict

Solutions:

  1. Check logs for errors:
docker compose logs
  1. Common errors:

Syntax Error:

SyntaxError: Unexpected token

→ Check server.js for missing commas, brackets, etc.

Module not found:

Error: Cannot find module 'simple-oauth2'

→ Rebuild container:

docker compose build --no-cache

Port in use:

Error starting userland proxy: listen tcp 0.0.0.0:3000: bind: address already in use

→ Another container using port 3000. Check with:

docker ps | grep 3000
  1. Test server.js syntax locally:
node -c server.js

Should return nothing if syntax is valid.

Issue 5: GitHub OAuth App Restrictions

Symptoms:

  • Error: “organization has enabled OAuth App access restrictions”
  • Authentication fails after authorization

Cause:

  • Organization has strict OAuth policies

Solution:

  1. Go to: https://github.com/organizations/ORG_NAME/settings/oauth_application_policy
  2. Find your OAuth App in the list
  3. Click Grant access or Request access
  4. If you’re org owner, access grants immediately
  5. If not, org admin must approve

Issue 6: Sites Work Individually but Not Together

Symptoms:

  • Site 1 works fine
  • Adding Site 2 breaks Site 1
  • Authentication fails for both

Causes:

  • Duplicate site keys
  • Environment variable conflicts
  • Syntax error breaking all sites

Solutions:

  1. Check for duplicate keys:
cd /opt/docker/decap-oauth
grep -n "^  [a-z]*:" server.js

Each site should appear only once.

  1. Test configuration endpoint:
curl https://oauth.boeygembos.be/ | jq

Should list all sites. If it errors, there’s a syntax problem.

  1. Roll back to working version:
cp server.js.backup server.js
docker compose restart

Then add sites one at a time, testing after each.


Maintenance

Monitoring

Check Provider Status

# Health check
curl https://oauth.boeygembos.be/health

# List configured sites
curl https://oauth.boeygembos.be/ | jq '.sites'

# View container logs
cd /opt/docker/decap-oauth
docker compose logs --tail=50

Watch Live Traffic

docker compose logs -f

You’ll see:

  • Auth requests: Auth request for site: sitename
  • Successful auth: Successfully authenticated for site: sitename
  • Errors: Any authentication failures

Check Container Health

docker ps | grep decap-oauth

# Should show:
# - STATUS: Up X minutes/hours
# - PORTS: Not externally exposed (Traefik handles routing)

Updating Sites

Modify Existing Site

  1. Update server.js (e.g., change allowedOrigins)
nano /opt/docker/decap-oauth/server.js
# Make changes
  1. Restart container:
docker compose restart

No rebuild needed for config changes.

Remove a Site

  1. Delete from server.js:
// Remove the entire site object
  1. Remove from docker-compose.yml:
# Remove SITENAME_CLIENT_ID and SITENAME_CLIENT_SECRET
  1. Restart:
docker compose down
docker compose build --no-cache
docker compose up -d
  1. Revoke GitHub OAuth App (optional but recommended):
  • Go to: https://github.com/settings/developers
  • Find the OAuth App
  • Click Delete

Rotating Secrets

If a Client Secret is compromised:

  1. Generate new secret on GitHub:
  • Settings → Developer settings → OAuth Apps
  • Find the app
  • Click Revoke all user tokens (optional but recommended)
  • Click Generate a new client secret
  • Copy new secret
  1. Update docker-compose.yml:
nano docker-compose.yml
# Update SITENAME_CLIENT_SECRET
  1. Restart container:
docker compose restart
  1. Test authentication:
  • Users will need to re-authorize
  • Existing sessions will be invalidated

Backing Up Configuration

Backup regularly:

cd /opt/docker/decap-oauth

# Create backup directory
mkdir -p ../backups/decap-oauth

# Backup with timestamp
DATE=$(date +%Y%m%d-%H%M%S)
tar -czf ../backups/decap-oauth/backup-$DATE.tar.gz .

# Keep last 10 backups
cd ../backups/decap-oauth
ls -t | tail -n +11 | xargs rm -f

Restore from backup:

cd /opt/docker/decap-oauth
docker compose down

# Extract backup (replace DATE with actual backup)
tar -xzf ../backups/decap-oauth/backup-DATE.tar.gz

docker compose up -d

Updating Dependencies

Check for updates:

cd /opt/docker/decap-oauth

# Check current versions
docker exec decap-oauth npm list --depth=0

# Update package.json with latest versions
nano package.json

Update and rebuild:

# Update package.json
nano package.json

# Rebuild
docker compose down
docker compose build --no-cache
docker compose up -d

# Test all sites

Test thoroughly after dependency updates!

Logs Management

View logs:

docker compose logs --tail=100
docker compose logs -f  # Follow mode
docker compose logs --since=1h  # Last hour

Log rotation (handled by Docker automatically):

  • Default: Logs rotate at 10MB
  • Keeps last 3 files
  • No action needed

Manual cleanup (if needed):

docker compose down
docker system prune -a  # Remove all unused data
docker compose up -d

Security Best Practices

Secrets Management

✅ DO:

  • Store Client Secrets in docker-compose.yml only
  • Set file permissions: chmod 600 docker-compose.yml
  • Keep backups secure and encrypted
  • Rotate secrets periodically (every 6-12 months)
  • Use different credentials for each site

❌ DON’T:

  • Commit docker-compose.yml to git with secrets
  • Share Client Secrets via insecure channels
  • Reuse same credentials across sites
  • Store secrets in plain text files
  • Log secrets to console

Access Control

GitHub Repository Access:

  • Only add collaborators who need CMS access
  • Review access regularly
  • Remove access for former team members
  • Use teams instead of individual access when possible

OAuth App Access:

  • Don’t approve unknown OAuth apps
  • Review authorized apps: https://github.com/settings/applications
  • Revoke unused apps
  • Monitor organization OAuth policies

Network Security

Container Isolation:

  • OAuth provider only on web network
  • No direct port exposure (3000 is internal only)
  • Traffic always through Traefik (HTTPS)

Firewall:

  • Only ports 80, 443 open on VPS
  • SSH restricted to key-only auth
  • fail2ban monitoring enabled

Monitoring

Watch for:

  • Unusual authentication patterns
  • Failed authentication attempts
  • Unknown sites in logs
  • Unexpected traffic spikes

Set up alerts (optional but recommended):

  • UptimeRobot for provider availability
  • GitHub notifications for OAuth app changes
  • Log monitoring for errors

Performance Optimization

Current Performance

Typical metrics:

  • Authentication time: 1-2 seconds
  • Memory usage: ~50-100 MB
  • CPU usage: Minimal (< 1%)
  • Can handle 50+ sites easily

Scaling Considerations

When to optimize:

  • 50+ configured sites
  • High traffic (1000+ auth/day)
  • Slow authentication times
  • Memory issues

Optimization strategies:

  1. Enable caching (for very high traffic):
// Add to server.js
const NodeCache = require('node-cache');
const tokenCache = new NodeCache({ stdTTL: 600 }); // 10 min cache
  1. Rate limiting (prevent abuse):
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/auth/', limiter);
  1. Horizontal scaling (load balancing):
  • Deploy multiple OAuth provider instances
  • Use Traefik load balancing
  • Share state via Redis

For most use cases, no optimization needed!


Advanced Configurations

Multiple Domains for Same Site

If a site is accessible via multiple domains:

allowedOrigins: [
  'https://www.site.com',
  'https://site.com',
  'https://staging.site.com'
]

Different Git Providers

GitLab example:

clientsite: {
  client: new AuthorizationCode({
    client: {
      id: process.env.CLIENTSITE_CLIENT_ID,
      secret: process.env.CLIENTSITE_CLIENT_SECRET,
    },
    auth: {
      tokenHost: 'https://gitlab.com',  // Changed
      tokenPath: '/oauth/token',  // Changed
      authorizePath: '/oauth/authorize',  // Changed
    },
  }),
  allowedOrigins: ['https://www.clientsite.com'],
  description: 'Client Site (GitLab)'
}

Bitbucket similar to GitLab.

Custom Authentication Messages

Customize the callback HTML in server.js:

// In /callback/:site route
const script = `
  <!DOCTYPE html>
  <html>
  <head>
    <title>Welcome to ${siteName}!</title>
    <style>
      /* Your custom styles */
    </style>
  </head>
  <body>
    <div class="message">
      <h1>✓ Welcome!</h1>
      <p>Successfully logged into ${clientConfig.description}</p>
    </div>
    <script>
      // postMessage code here
    </script>
  </body>
  </html>
`;

Logging to External Service

Add structured logging:

// At top of server.js
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Use in routes
logger.info('Auth request', { site: siteName, timestamp: new Date() });

Migration Guide

From Single-Site to Multi-Site

If you have existing single-site OAuth providers and want to consolidate:

  1. Inventory existing providers:
# List all OAuth containers
docker ps -a | grep oauth
  1. Document each site’s credentials:
  • GitHub OAuth App Client ID
  • GitHub OAuth App Client Secret
  • Site domain
  • Repository
  1. Set up multi-site provider (follow Initial Setup in this guide)
  2. Add all sites to multi-site config
  3. Update each site’s Decap CMS config:
base_url: https://oauth.boeygembos.be
auth_endpoint: auth/SITENAME
  1. Test each site individually
  2. Remove old single-site containers:
cd /opt/docker/old-oauth-provider
docker compose down
  1. Update DNS if needed (if using site-specific OAuth domains)

From Netlify/External OAuth

If migrating from Netlify or another OAuth service:

  1. Create GitHub OAuth Apps for each site
  2. Deploy multi-site provider on VPS
  3. Update Decap CMS configs to point to your provider
  4. Test thoroughly before removing old OAuth
  5. Update in batches to minimize risk

Cost Analysis

Resource Usage

Single OAuth provider serving 10 sites:

  • Memory: ~100 MB
  • CPU: < 0.5%
  • Disk: ~50 MB
  • Network: Minimal

Vs. 10 separate OAuth containers:

  • Memory: ~1 GB (10x)
  • CPU: ~5% (10x)
  • Disk: ~500 MB (10x)
  • Management overhead: 10x

Savings

VPS resources saved:

  • Can host 50+ sites vs 10 with separate containers
  • Lower memory pressure
  • Faster deployments
  • Simpler architecture

Time saved:

  • Add new site: 15 min vs 45 min
  • Updates: One change vs updating each container
  • Monitoring: One service vs many

Troubleshooting Decision Tree

Authentication not working?
    ├─ Error: "Unknown site"
    │   ├─ Check site key matches in server.js and config.yml
    │   └─ Verify container restarted after adding site
    │
    ├─ Error: 401 Unauthorized
    │   ├─ Verify Client ID/Secret in docker-compose.yml
    │   └─ Check if secret was regenerated on GitHub
    │
    ├─ CORS Error in browser
    │   ├─ Check allowedOrigins includes correct domain
    │   └─ Verify protocol (http vs https) matches
    │
    ├─ Popup closes immediately
    │   └─ Check browser console for errors
    │
    └─ OAuth provider not accessible
        ├─ Check DNS: dig oauth.boeygembos.be
        ├─ Check container: docker ps | grep decap-oauth
        └─ Check logs: docker compose logs

FAQ

Q: How many sites can one provider handle?

A: Theoretically unlimited. Practically tested with 50+ sites without issues. Limited only by VPS resources.

Q: Can I use different OAuth providers (GitHub, GitLab) together?

A: Yes! Each site in the clients object can configure different OAuth providers.

Q: What if one site’s OAuth App is compromised?

A: Only that site is affected. Regenerate its Client Secret and update docker-compose.yml. Other sites unaffected.

Q: Can I use custom domains for the OAuth provider?

A: Yes, but not recommended. Using oauth.boeygembos.be for all sites simplifies management. If needed, configure multiple Traefik routes.

Q: Do users need to authorize each time?

A: No. GitHub stores authorization. Users only authorize once per OAuth App (unless token expires or is revoked).

Q: Can I restrict which GitHub users can access each site?

A: Yes, via GitHub repository permissions. Only users with write access to the repo can save changes via CMS.

Q: What happens if the OAuth provider is down?

A: Users can’t log into CMS, but websites remain accessible. OAuth only needed for CMS authentication, not site access.

Q: Can I test locally before deploying?

A: Yes. Run the OAuth provider locally with ngrok or similar tunnel, then update base_url in test site’s config.yml to point to your local instance.