Files
firefrost-operations-manual/docs/implementation/stateless-oauth-bridge-implementation.md
Claude (Chronicler #58) d9f033aa02 docs: Add Stateless OAuth Bridge implementation guide
The complete implementation guide from The Validator (#57) and Gemini.
Used by Chronicler #58 to implement Discord-Stripe OAuth linking.

Contains:
- Step-by-step implementation instructions
- OAuth route code (/stripe/auth, /stripe/callback)
- Webhook handler updates
- Website button changes
- Testing checklist
- Troubleshooting guide

Signed-off-by: Claude <claude@firefrostgaming.com>
2026-04-04 00:18:27 +00:00

13 KiB

Discord-Stripe OAuth Linking - Complete Implementation Guide

🎯 Mission: Automate Discord User → Stripe Subscription Linking

Current Problem: Subscriptions created via Stripe checkout have NO discord_id, making them invisible in admin panel and preventing Discord role assignment.

Gemini's Solution: "Stateless OAuth Bridge" - Use Discord OAuth before Stripe checkout, pass Discord ID through the entire flow via Stripe's client_reference_id field.


📋 Prerequisites

Before implementing, verify these exist:

  1. Discord Application Settings:

    • Client ID: 1487080166969577502
    • Client Secret: xOK9ZYgionyqd-huGJRE2Rym98zy0W-m
    • Current redirect URI: https://discord-bot.firefrostgaming.com/auth/discord/callback (for admin panel)
    • NEW redirect URI needed: https://discord-bot.firefrostgaming.com/stripe/callback (for checkout flow)
  2. Environment Variables in /opt/arbiter-3.0/.env:

    DISCORD_CLIENT_ID=1487080166969577502
    DISCORD_CLIENT_SECRET=xOK9ZYgionyqd-huGJRE2Rym98zy0W-m
    DISCORD_CHECKOUT_REDIRECT_URI=https://discord-bot.firefrostgaming.com/stripe/callback
    
  3. Database:

    • stripe_products table with tier_level and stripe_price_id columns
    • subscriptions table with discord_id column
  4. Stripe Integration:

    • Webhook endpoint at /webhooks/stripe
    • Handles checkout.session.completed event

🔧 Implementation Steps

Step 1: Add Discord Redirect URI

Action: Go to Discord Developer Portal → Applications → OAuth2 → Redirects

Add:

https://discord-bot.firefrostgaming.com/stripe/callback

Why: Discord will redirect users back to this URL after login.


Step 2: Add Environment Variable

File: /opt/arbiter-3.0/.env

Add this line:

DISCORD_CHECKOUT_REDIRECT_URI=https://discord-bot.firefrostgaming.com/stripe/callback

Verify it loaded:

systemctl restart arbiter-3
journalctl -u arbiter-3 -n 20 | grep -i "listening"

Step 3: Install axios (if not already installed)

Command:

cd /opt/arbiter-3.0 && npm install axios

Why: We need to make HTTP requests to Discord's OAuth API.


Step 4: Add OAuth Routes to stripe.js

File: /opt/arbiter-3.0/src/routes/stripe.js

Add these TWO routes BEFORE the checkout route:

Route 1: Auth Entry Point

const axios = require('axios');

// Step 1: Entry point - User clicks subscribe button on website
router.get('/auth', (req, res) => {
  const tierLevel = req.query.tier;
  
  if (!tierLevel) {
    return res.status(400).send('❌ Tier level is required');
  }
  
  // Redirect to Discord OAuth with tier in state parameter
  const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.DISCORD_CHECKOUT_REDIRECT_URI)}&response_type=code&scope=identify&state=${tierLevel}`;
  
  res.redirect(discordAuthUrl);
});

Route 2: OAuth Callback → Create Stripe Session

// Step 2: Discord redirects back here with user ID and tier
router.get('/callback', async (req, res) => {
  const { code, state: tierLevel } = req.query;
  
  if (!code || !tierLevel) {
    return res.redirect('https://firefrostgaming.com/subscribe?error=missing_params');
  }
  
  try {
    // Exchange OAuth code for Discord access token
    const tokenResponse = await axios.post(
      'https://discord.com/api/oauth2/token',
      new URLSearchParams({
        client_id: process.env.DISCORD_CLIENT_ID,
        client_secret: process.env.DISCORD_CLIENT_SECRET,
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: process.env.DISCORD_CHECKOUT_REDIRECT_URI
      }),
      { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
    );
    
    // Fetch Discord user identity
    const userResponse = await axios.get('https://discord.com/api/users/@me', {
      headers: { Authorization: `Bearer ${tokenResponse.data.access_token}` }
    });
    
    const discordId = userResponse.data.id;
    const discordUsername = userResponse.data.username;
    
    console.log(`✅ Discord OAuth success: ${discordUsername} (${discordId}) purchasing tier ${tierLevel}`);
    
    // Get Stripe price ID from database
    const productResult = await pool.query(
      'SELECT stripe_price_id, tier_name, billing_type FROM stripe_products WHERE tier_level = $1',
      [tierLevel]
    );
    
    if (productResult.rows.length === 0) {
      return res.redirect('https://firefrostgaming.com/subscribe?error=invalid_tier');
    }
    
    const { stripe_price_id, tier_name, billing_type } = productResult.rows[0];
    
    // Create Stripe Checkout Session with Discord ID attached
    const session = await stripe.checkout.sessions.create({
      mode: billing_type === 'recurring' ? 'subscription' : 'payment',
      line_items: [{ price: stripe_price_id, quantity: 1 }],
      success_url: 'https://firefrostgaming.com/success?session_id={CHECKOUT_SESSION_ID}',
      cancel_url: 'https://firefrostgaming.com/subscribe',
      client_reference_id: discordId, // 🔥 THIS IS THE MAGIC - Discord ID travels to webhook!
      metadata: {
        tier_level: tierLevel,
        tier_name: tier_name,
        discord_username: discordUsername
      }
    });
    
    console.log(`✅ Stripe session created: ${session.id} for ${discordUsername}`);
    
    // Redirect user to Stripe payment page
    res.redirect(session.url);
    
  } catch (error) {
    console.error('❌ OAuth/Checkout Error:', error.response?.data || error.message);
    res.redirect('https://firefrostgaming.com/subscribe?error=auth_failed');
  }
});

Step 5: Update Webhook Handler

File: /opt/arbiter-3.0/src/routes/stripe.js

Find the checkout.session.completed handler and UPDATE it:

case 'checkout.session.completed': {
  const session = event.data.object;
  
  // 🔥 CRITICAL: Extract Discord ID from client_reference_id
  const discordId = session.client_reference_id;
  const customerEmail = session.customer_details.email;
  const tierLevel = parseInt(session.metadata.tier_level);
  const tierName = session.metadata.tier_name;
  
  if (!discordId) {
    console.error('❌ No Discord ID in checkout session:', session.id);
    return res.status(400).send('Missing Discord ID');
  }
  
  console.log(`✅ Payment complete: ${session.metadata.discord_username} (${discordId}) - ${tierName}`);
  
  // Determine subscription ID based on billing type
  let stripeSubscriptionId = null;
  let status = 'lifetime';
  
  if (session.mode === 'subscription') {
    stripeSubscriptionId = session.subscription;
    status = 'active';
  }
  
  // Insert/update subscription with Discord ID
  await pool.query(`
    INSERT INTO subscriptions (
      stripe_subscription_id, 
      stripe_customer_id, 
      discord_id, 
      tier_level, 
      status,
      created_at
    )
    VALUES ($1, $2, $3, $4, $5, NOW())
    ON CONFLICT (discord_id) 
    DO UPDATE SET 
      tier_level = EXCLUDED.tier_level,
      status = EXCLUDED.status,
      stripe_subscription_id = EXCLUDED.stripe_subscription_id,
      stripe_customer_id = EXCLUDED.stripe_customer_id,
      updated_at = NOW()
  `, [stripeSubscriptionId, session.customer, discordId, tierLevel, status]);
  
  console.log(`✅ Database updated: Discord ${discordId} → Tier ${tierLevel} (${status})`);
  
  // TODO: Trigger Discord role assignment via Arbiter
  // This will be implemented in future task
  
  break;
}

Step 6: Update Website Buttons

File: Ghost CMS website (firefrostgaming.com)

BEFORE (old direct Stripe buttons):

<a href="https://buy.stripe.com/..." class="btn">Subscribe to Awakened</a>

AFTER (new OAuth flow buttons):

<!-- Awakened - Tier 1 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=1" class="btn">
  Subscribe to Awakened ($1)
</a>

<!-- Elemental Fire - Tier 2 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=2" class="btn">
  Subscribe to Elemental (Fire) ($5/mo)
</a>

<!-- Elemental Frost - Tier 3 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=3" class="btn">
  Subscribe to Elemental (Frost) ($5/mo)
</a>

<!-- Knight Fire - Tier 4 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=4" class="btn">
  Subscribe to Knight (Fire) ($10/mo)
</a>

<!-- Knight Frost - Tier 5 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=5" class="btn">
  Subscribe to Knight (Frost) ($10/mo)
</a>

<!-- Master Fire - Tier 6 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=6" class="btn">
  Subscribe to Master (Fire) ($15/mo)
</a>

<!-- Master Frost - Tier 7 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=7" class="btn">
  Subscribe to Master (Frost) ($15/mo)
</a>

<!-- Legend Fire - Tier 8 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=8" class="btn">
  Subscribe to Legend (Fire) ($20/mo)
</a>

<!-- Legend Frost - Tier 9 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=9" class="btn">
  Subscribe to Legend (Frost) ($20/mo)
</a>

<!-- Sovereign - Tier 10 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=10" class="btn btn-sovereign">
  Subscribe to Sovereign ($499)
</a>

🧪 Testing the Flow

Test 1: Complete Checkout Flow

  1. Click subscribe button on website
  2. Should redirect to Discord login
  3. Log in with Discord
  4. Should redirect to Stripe checkout
  5. Complete test payment
  6. Check database:
    SELECT id, discord_id, tier_level, status 
    FROM subscriptions 
    ORDER BY created_at DESC 
    LIMIT 5;
    
  7. EXPECTED: New row with your Discord ID and correct tier_level

Test 2: Verify in Admin Panel

  1. Go to https://discord-bot.firefrostgaming.com/admin/players
  2. EXPECTED: See your Discord username, avatar, and tier badge
  3. NOT: See "N/A" or "Unlinked"

Test 3: Multiple Tiers

  1. Test purchasing different tiers (1, 5, 10)
  2. Verify each creates correct database entry
  3. Verify admin panel shows correct tier name

🚨 Troubleshooting

Issue: "Tier level is required" error

Cause: Button missing ?tier=X parameter

Fix: Update website button URL to include tier number


Issue: Discord OAuth fails with "Invalid redirect_uri"

Cause: Redirect URI not added to Discord Developer Portal

Fix:

  1. Go to Discord Developer Portal
  2. Add https://discord-bot.firefrostgaming.com/stripe/callback
  3. Save changes

Issue: Webhook receives no Discord ID

Cause: client_reference_id not being set in session creation

Fix: Verify callback route includes:

client_reference_id: discordId

Issue: Database shows NULL discord_id

Cause: Webhook using old code that doesn't extract client_reference_id

Fix: Update webhook handler per Step 5


Success Criteria

You'll know it works when:

  1. User clicks subscribe → redirects to Discord login
  2. After Discord login → redirects to Stripe checkout
  3. After payment → subscription in database has discord_id filled in
  4. Admin panel Players page shows Discord username + avatar
  5. No "N/A" or "Unlinked" entries for new subscriptions

📊 Database Schema Reference

subscriptions table:

CREATE TABLE subscriptions (
  id SERIAL PRIMARY KEY,
  stripe_subscription_id VARCHAR(255),
  stripe_customer_id VARCHAR(255),
  discord_id VARCHAR(255), -- This gets filled by OAuth flow!
  tier_level INTEGER,
  status VARCHAR(50),
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

stripe_products table:

CREATE TABLE stripe_products (
  tier_level INTEGER PRIMARY KEY,
  tier_name VARCHAR(255),
  stripe_price_id VARCHAR(255),
  billing_type VARCHAR(50) -- 'recurring' or 'one-time'
);

🎯 Next Steps After Implementation

  1. Test with real Discord accounts (use test mode Stripe cards)
  2. Verify admin panel shows linked subscriptions
  3. Update Ghost CMS website buttons to use new OAuth flow
  4. Remove old direct Stripe checkout links
  5. Implement Discord role assignment (future task - Arbiter will auto-assign roles based on tier_level)

🔥 Why This Architecture Is Perfect

Stateless: No cookies, no sessions, no CSRF tokens needed

Secure: Discord OAuth validates user identity

Automatic: Zero manual linking required

Scalable: Works for 1 user or 10,000 users

Simple: Only 2 new routes, minimal code changes

RV-Ready: Runs completely automated while Michael travels


📁 Files Modified

  1. /opt/arbiter-3.0/.env - Add DISCORD_CHECKOUT_REDIRECT_URI
  2. /opt/arbiter-3.0/src/routes/stripe.js - Add /auth and /callback routes, update webhook
  3. Ghost CMS website - Update subscribe button URLs
  4. Discord Developer Portal - Add new redirect URI

🎉 Final Note

This is the LAST piece of the payment puzzle! Once this is implemented and tested, Firefrost Gaming has a fully automated subscription system that:

  • Takes payments via Stripe
  • Links Discord users automatically
  • Shows subscribers in admin panel
  • Tracks revenue in Financials
  • Ready for Discord role auto-assignment (future task) 🔜

SOFT LAUNCH READY! 🚀🔥❄️


Credit: Gemini's "Stateless OAuth Bridge" architecture Documented by: The Validator (Chronicler #57) Date: April 3, 2026