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>
This commit is contained in:
472
docs/implementation/stateless-oauth-bridge-implementation.md
Normal file
472
docs/implementation/stateless-oauth-bridge-implementation.md
Normal file
@@ -0,0 +1,472 @@
|
||||
# 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`:**
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
DISCORD_CHECKOUT_REDIRECT_URI=https://discord-bot.firefrostgaming.com/stripe/callback
|
||||
```
|
||||
|
||||
**Verify it loaded:**
|
||||
```bash
|
||||
systemctl restart arbiter-3
|
||||
journalctl -u arbiter-3 -n 20 | grep -i "listening"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Install axios (if not already installed)
|
||||
|
||||
**Command:**
|
||||
```bash
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
// 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:**
|
||||
|
||||
```javascript
|
||||
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):**
|
||||
```html
|
||||
<a href="https://buy.stripe.com/..." class="btn">Subscribe to Awakened</a>
|
||||
```
|
||||
|
||||
**AFTER (new OAuth flow buttons):**
|
||||
```html
|
||||
<!-- 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:
|
||||
```sql
|
||||
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:
|
||||
```javascript
|
||||
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:**
|
||||
```sql
|
||||
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:**
|
||||
```sql
|
||||
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*
|
||||
Reference in New Issue
Block a user