From 773cc74c33dff4adff324cef2659ad5a1eb56726 Mon Sep 17 00:00:00 2001 From: MrPiglr <31398225+MrPiglr@users.noreply.github.com> Date: Wed, 24 Dec 2025 04:15:25 +0000 Subject: [PATCH] Add OAuth 2.0 implementation with secure credential handling - Implement server-side OAuth handlers for Discord, Roblox, GitHub - Add OAuth routes with state validation and PKCE support - Create comprehensive documentation (setup, rotation, quickstart) - Add .env to .gitignore to protect credentials --- .gitignore | 7 +- docs/CREDENTIALS_ROTATION.md | 357 +++++++++++++++++++++++++++++++++ docs/OAUTH_IMPLEMENTATION.md | 307 +++++++++++++++++++++++++++++ docs/OAUTH_QUICKSTART.md | 140 +++++++++++++ docs/OAUTH_SETUP.md | 261 +++++++++++++++++++++++++ server/oauth-handlers.ts | 368 +++++++++++++++++++++++++++++++++++ server/routes.ts | 9 + 7 files changed, 1448 insertions(+), 1 deletion(-) create mode 100644 docs/CREDENTIALS_ROTATION.md create mode 100644 docs/OAUTH_IMPLEMENTATION.md create mode 100644 docs/OAUTH_QUICKSTART.md create mode 100644 docs/OAUTH_SETUP.md create mode 100644 server/oauth-handlers.ts diff --git a/.gitignore b/.gitignore index f9ba7f8..46aaaaa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,9 @@ dist .DS_Store server/public vite.config.ts.* -*.tar.gz \ No newline at end of file +*.tar.gz + +# Environment variables +.env +.env.local +.env.*.local \ No newline at end of file diff --git a/docs/CREDENTIALS_ROTATION.md b/docs/CREDENTIALS_ROTATION.md new file mode 100644 index 0000000..b5fb600 --- /dev/null +++ b/docs/CREDENTIALS_ROTATION.md @@ -0,0 +1,357 @@ +# Credentials Rotation Guide + +## ๐Ÿšจ Security Incident Response + +**If credentials are compromised (e.g., accidentally committed to git or shared publicly), follow this guide IMMEDIATELY.** + +--- + +## ๐Ÿ”„ Rotation Priority Order + +### ๐Ÿ”ด CRITICAL (Rotate Immediately) +1. **Discord Bot Token** - Full bot control +2. **Stripe Secret Key** - Payment processing access +3. **GitHub Personal Access Token** - Repository access + +### ๐ŸŸก HIGH (Rotate Before Production) +4. **Discord Client Secret** - OAuth access +5. **Roblox Client Secret** - OAuth access +6. **Roblox Open Cloud API Key** - API access +7. **Stripe Webhook Secret** - Webhook validation + +### ๐ŸŸข MEDIUM (Rotate When Convenient) +8. **Discord Public Key** - Webhook signature verification +9. **GitHub Client ID/Secret** - OAuth (once registered) + +--- + +## ๐ŸŽฎ Discord Credentials Rotation + +### 1. Bot Token +**Why:** Full control over bot actions, can read/send messages, access servers + +1. Go to [Discord Developer Portal](https://discord.com/developers/applications) +2. Select your application +3. Navigate to **Bot** section +4. Click **Reset Token** +5. Copy new token to `.env`: + ```bash + DISCORD_BOT_TOKEN=NEW_TOKEN_HERE + ``` +6. Restart your application + +### 2. Client Secret +**Why:** Used for OAuth token exchange + +1. In Discord Developer Portal, go to **OAuth2** section +2. Click **Reset Secret** +3. Copy new secret to `.env`: + ```bash + DISCORD_CLIENT_SECRET=NEW_SECRET_HERE + ``` +4. Restart your application + +### 3. Public Key +**Why:** Used to verify webhook signatures (less critical but good practice) + +1. In **General Information** section +2. Click **Regenerate** next to Public Key +3. Update `.env`: + ```bash + DISCORD_PUBLIC_KEY=NEW_KEY_HERE + ``` + +--- + +## ๐ŸŽฒ Roblox Credentials Rotation + +### 1. Client Secret +**Why:** Used for OAuth token exchange + +1. Go to [Roblox Creator Dashboard](https://create.roblox.com/dashboard/credentials) +2. Find your OAuth 2.0 credential +3. Click **Regenerate Secret** +4. Copy new secret to `.env`: + ```bash + ROBLOX_CLIENT_SECRET=NEW_SECRET_HERE + ``` +5. Restart your application + +### 2. Open Cloud API Key +**Why:** Server-to-server API access + +1. In Creator Dashboard, go to **API Keys** +2. Find the compromised key +3. Click **Delete** to revoke it +4. Create new API key with same permissions +5. Copy to `.env`: + ```bash + ROBLOX_OPEN_CLOUD_API_KEY=NEW_KEY_HERE + ``` +6. Restart your application + +**Note:** Old API key stops working immediately upon deletion. + +--- + +## ๐Ÿ™ GitHub Credentials Rotation + +### 1. Personal Access Token +**Why:** Repository and API access + +1. Go to [GitHub Personal Access Tokens](https://github.com/settings/tokens) +2. Find the compromised token +3. Click **Delete** to revoke it +4. Generate new token: + - Click **Generate new token (classic)** + - Select same scopes as before + - Set expiration (recommend 90 days) +5. Copy to `.env`: + ```bash + GITHUB_PERSONAL_ACCESS_TOKEN=NEW_TOKEN_HERE + ``` +6. Restart your application + +**Note:** Old token stops working immediately upon deletion. + +### 2. OAuth Client Secret +**When you register OAuth app:** + +1. Go to [GitHub Developer Settings](https://github.com/settings/developers) +2. Select your OAuth app +3. Click **Generate a new client secret** +4. Copy to `.env`: + ```bash + GITHUB_CLIENT_SECRET=NEW_SECRET_HERE + ``` + +--- + +## ๐Ÿ’ณ Stripe Credentials Rotation + +### 1. Secret Key +**Why:** Full payment processing access - HIGHEST RISK + +1. Go to [Stripe Dashboard](https://dashboard.stripe.com/apikeys) +2. Click **Reveal test key** or **Reveal live key** +3. Click **Roll secret key** +4. Confirm the rollover +5. Copy new key to `.env`: + ```bash + STRIPE_SECRET_KEY=NEW_KEY_HERE + ``` +6. **Deploy immediately** - old key has grace period + +**โš ๏ธ Important:** +- Old key works for 24-48 hours (grace period) +- Deploy new key ASAP to avoid disruption +- Test payments after deployment + +### 2. Webhook Secret +**Why:** Validates webhook authenticity + +1. Go to **Developers** โ†’ **Webhooks** +2. Click your webhook endpoint +3. Click **Roll secret** +4. Copy new secret to `.env`: + ```bash + STRIPE_WEBHOOK_SECRET=NEW_SECRET_HERE + ``` +5. Restart your application + +**Note:** Old webhooks will fail signature validation immediately. + +--- + +## ๐Ÿ” Supabase Credentials (If Needed) + +### Anon Key +**Lower risk** - designed to be public, but rotation doesn't hurt + +1. Go to [Supabase Dashboard](https://supabase.com/dashboard/project/_/settings/api) +2. Navigate to **Settings** โ†’ **API** +3. Click **Generate new anon key** (if available) +4. Update `.env`: + ```bash + SUPABASE_ANON_KEY=NEW_KEY_HERE + VITE_SUPABASE_ANON_KEY=NEW_KEY_HERE + ``` + +### Service Role Key +**CRITICAL** - full database access + +1. In Supabase Dashboard, go to **Settings** โ†’ **API** +2. Click **Rotate service_role key** +3. Update server-side env (never expose to client) +4. Restart all server instances + +--- + +## โœ… Post-Rotation Checklist + +After rotating credentials: + +### 1. Environment Variables +- [ ] Updated `.env` file with all new credentials +- [ ] Verified no typos in new keys +- [ ] Confirmed `.env` is in `.gitignore` +- [ ] Deleted old `.env` backups + +### 2. Application Deployment +- [ ] Restarted local development server +- [ ] Tested OAuth flows with new credentials +- [ ] Verified webhook signatures validate +- [ ] Tested API calls work correctly + +### 3. Production Deployment (When Ready) +- [ ] Updated production environment variables +- [ ] Deployed application with zero downtime +- [ ] Monitored logs for authentication errors +- [ ] Verified no legacy credential usage + +### 4. Documentation +- [ ] Updated internal team docs with new setup +- [ ] Documented rotation date in security log +- [ ] Set calendar reminder for next rotation (90 days) + +### 5. Access Control +- [ ] Removed compromised credentials from all locations: + - [ ] Chat logs (can't delete, but rotate makes them useless) + - [ ] Clipboard history + - [ ] Shell history (`history -c`) + - [ ] Git reflog (if accidentally committed) + +--- + +## ๐Ÿ—“๏ธ Rotation Schedule + +### Recommended Rotation Frequency + +| Credential | Frequency | Priority | +|------------|-----------|----------| +| Stripe Secret Key | Every 90 days | ๐Ÿ”ด Critical | +| Bot Tokens | Every 90 days | ๐Ÿ”ด Critical | +| Personal Access Tokens | Every 90 days | ๐ŸŸก High | +| OAuth Client Secrets | Every 180 days | ๐ŸŸก High | +| API Keys | Every 180 days | ๐ŸŸก High | +| Webhook Secrets | Every 180 days | ๐ŸŸข Medium | +| Public Keys | Annually | ๐ŸŸข Medium | + +### Set Reminders +```bash +# Add to calendar or use cron job: +0 0 1 */3 * * echo "Rotate Stripe/Discord credentials" | mail admin@aethex.app +``` + +--- + +## ๐Ÿšจ Git History Cleanup (If Committed) + +**If credentials were accidentally committed to git:** + +### Option 1: BFG Repo-Cleaner (Recommended) +```bash +# Install BFG +brew install bfg # or download from https://rtyley.github.io/bfg-repo-cleaner/ + +# Clone a fresh copy +git clone --mirror https://github.com/AeThex-Corporation/AeThex-OS.git + +# Remove .env files from history +bfg --delete-files .env AeThex-OS.git + +# Clean up +cd AeThex-OS.git +git reflog expire --expire=now --all +git gc --prune=now --aggressive + +# Force push (โš ๏ธ DESTRUCTIVE) +git push --force +``` + +### Option 2: git-filter-repo +```bash +# Install git-filter-repo +pip install git-filter-repo + +# Remove .env from history +git filter-repo --path .env --invert-paths + +# Force push +git push origin --force --all +``` + +**โš ๏ธ Warning:** Force pushing rewrites history. Coordinate with team! + +--- + +## ๐Ÿ“ž Emergency Contacts + +If credentials are actively being abused: + +### Discord +- **Report abuse:** https://dis.gd/report +- **Developer support:** https://discord.com/developers/docs + +### Stripe +- **Emergency contact:** https://support.stripe.com/ +- **Phone support:** Available for paid plans + +### GitHub +- **Security incidents:** security@github.com +- **Support:** https://support.github.com/ + +### Roblox +- **Security:** security@roblox.com +- **Support:** https://www.roblox.com/support + +--- + +## ๐Ÿงช Testing After Rotation + +Run these commands to verify new credentials work: + +```bash +# Test Discord OAuth +curl "https://discord.com/api/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&redirect_uri=http://localhost:5000/api/oauth/callback/discord&response_type=code&scope=identify" + +# Test Stripe API +curl https://api.stripe.com/v1/balance \ + -u ${STRIPE_SECRET_KEY}: + +# Test GitHub API +curl -H "Authorization: token ${GITHUB_PERSONAL_ACCESS_TOKEN}" \ + https://api.github.com/user + +# Test Roblox Open Cloud +curl -H "x-api-key: ${ROBLOX_OPEN_CLOUD_API_KEY}" \ + https://apis.roblox.com/cloud/v2/users/${USER_ID} +``` + +--- + +## ๐Ÿ“ Security Best Practices + +### Prevention +1. **Never commit credentials** - Use `.env` and add to `.gitignore` +2. **Use environment-specific credentials** - Separate dev/staging/prod +3. **Rotate proactively** - Don't wait for incidents +4. **Monitor usage** - Watch API logs for suspicious activity +5. **Least privilege** - Grant minimum permissions needed + +### Detection +1. **Enable webhook alerts** - Get notified of unusual API usage +2. **Monitor git commits** - Use pre-commit hooks to scan for secrets +3. **Audit logs** - Review provider dashboards regularly +4. **Automated scanning** - Use tools like `git-secrets` or `trufflehog` + +### Response +1. **Have this document ready** - Don't scramble during incidents +2. **Test rotation process** - Practice on dev environment first +3. **Document incidents** - Learn from mistakes +4. **Automate where possible** - Use secret management tools + +--- + +**Last Updated:** December 24, 2025 +**Next Review:** March 24, 2026 diff --git a/docs/OAUTH_IMPLEMENTATION.md b/docs/OAUTH_IMPLEMENTATION.md new file mode 100644 index 0000000..55d4b2c --- /dev/null +++ b/docs/OAUTH_IMPLEMENTATION.md @@ -0,0 +1,307 @@ +# OAuth Implementation Summary + +## โœ… What Was Implemented + +### 1. Server-Side OAuth Handler (`server/oauth-handlers.ts`) +- **Purpose:** Secure OAuth 2.0 identity linking for Discord, Roblox, and GitHub +- **Security:** Server-side identity verification prevents client-side spoofing +- **Features:** + - State token validation (5-minute TTL) + - PKCE support for Roblox OAuth + - Automatic subject/identity creation + - Duplicate identity detection + - Provider-specific identity mapping + +### 2. OAuth Routes (`server/routes.ts`) +Added two new endpoints: +- `POST /api/oauth/link/:provider` - Start OAuth flow (get authorization URL) +- `GET /api/oauth/callback/:provider` - OAuth callback handler + +### 3. Documentation +- **OAuth Setup Guide** (`docs/OAUTH_SETUP.md`) + - Provider registration instructions + - Redirect URI configuration + - Environment variable setup + - Testing procedures + +- **Credentials Rotation Guide** (`docs/CREDENTIALS_ROTATION.md`) + - Emergency response procedures + - Provider-specific rotation steps + - Security best practices + - Automated rotation schedules + +--- + +## ๐Ÿ”’ Security Improvements + +### Before (Vulnerable) +```typescript +// Client submits external_id - easily spoofed! +POST /api/link { provider: "discord", external_id: "123" } +``` + +### After (Secure) +```typescript +// 1. Client requests authorization URL +POST /api/oauth/link/discord +โ†’ Returns: { authUrl: "https://discord.com/...", state: "..." } + +// 2. User authorizes on Discord +โ†’ Redirects to callback with code + +// 3. Server exchanges code for token +GET /api/oauth/callback/discord?code=abc&state=xyz +โ†’ Server calls Discord API to get real user ID +โ†’ Creates identity link with verified ID +``` + +**Key Security Features:** +- โœ… Server fetches identity from provider (can't be faked) +- โœ… State tokens prevent CSRF attacks +- โœ… PKCE prevents authorization code interception (Roblox) +- โœ… Duplicate identity detection (one provider account = one AeThex account) +- โœ… In-memory state storage with automatic cleanup + +--- + +## ๐Ÿš€ How to Use + +### For Users (Frontend Integration) + +```typescript +// 1. Start linking flow +const response = await fetch(`/api/oauth/link/discord`, { + method: 'POST', + credentials: 'include' // Include session cookie +}); + +const { authUrl, state } = await response.json(); + +// 2. Redirect user to provider +window.location.href = authUrl; + +// 3. User returns to /settings?oauth=success&provider=discord +// Check query params to show success message +``` + +### For Developers + +#### Testing Locally +1. Register OAuth apps with localhost redirect URIs: + - Discord: `http://localhost:5000/api/oauth/callback/discord` + - Roblox: `http://localhost:5000/api/oauth/callback/roblox` + - GitHub: `http://localhost:5000/api/oauth/callback/github` + +2. Add credentials to `.env`: + ```bash + DISCORD_CLIENT_ID=... + DISCORD_CLIENT_SECRET=... + ROBLOX_CLIENT_ID=... + ROBLOX_CLIENT_SECRET=... + GITHUB_CLIENT_ID=... + GITHUB_CLIENT_SECRET=... + ``` + +3. Start dev server: + ```bash + npm run dev + ``` + +4. Test flow: + - Log in to AeThex OS + - Go to Settings page + - Click "Link Discord" button + - Authorize on Discord + - Verify redirect back with success message + - Check database for new `aethex_subject_identities` row + +--- + +## ๐Ÿ“Š Database Changes + +### New Records Created + +When a user links Discord account: + +**aethex_subjects** (if first identity): +```sql +INSERT INTO aethex_subjects (supabase_user_id) +VALUES ('uuid-of-supabase-user'); +``` + +**aethex_subject_identities**: +```sql +INSERT INTO aethex_subject_identities ( + subject_id, + issuer, + external_id, + external_username, + verified, + metadata +) VALUES ( + 'uuid-of-subject', + 'discord', + '123456789', + 'username#1234', + true, + '{"avatar": "...", "email": "...", "verified": true}' +); +``` + +### Querying Linked Identities + +```sql +-- Get all identities for a user +SELECT si.* +FROM aethex_subject_identities si +JOIN aethex_subjects s ON s.id = si.subject_id +WHERE s.supabase_user_id = 'user-uuid'; + +-- Check if Discord account already linked +SELECT s.supabase_user_id +FROM aethex_subject_identities si +JOIN aethex_subjects s ON s.id = si.subject_id +WHERE si.issuer = 'discord' + AND si.external_id = '123456789'; +``` + +--- + +## ๐Ÿ”ง Configuration + +### Environment Variables Required + +```bash +# Development +NODE_ENV=development +PORT=5000 + +# Discord OAuth +DISCORD_CLIENT_ID=your_dev_client_id +DISCORD_CLIENT_SECRET=your_dev_client_secret + +# Roblox OAuth +ROBLOX_CLIENT_ID=your_dev_client_id +ROBLOX_CLIENT_SECRET=your_dev_client_secret + +# GitHub OAuth +GITHUB_CLIENT_ID=your_dev_client_id +GITHUB_CLIENT_SECRET=your_dev_client_secret + +# Production +NODE_ENV=production +# ... same variables with production credentials +``` + +### Redirect URI Logic + +The handler automatically determines the correct redirect URI based on `NODE_ENV`: + +```typescript +function getRedirectUri(provider: string): string { + const baseUrl = process.env.NODE_ENV === "production" + ? "https://aethex.app" + : `http://localhost:${process.env.PORT || 5000}`; + + return `${baseUrl}/api/oauth/callback/${provider}`; +} +``` + +--- + +## ๐Ÿงช Testing Checklist + +### Manual Testing +- [ ] Start OAuth flow for each provider +- [ ] Complete authorization on provider site +- [ ] Verify redirect back to AeThex OS +- [ ] Check database for new identity record +- [ ] Try linking same provider account twice (should succeed, no duplicate) +- [ ] Try linking already-linked account to different AeThex user (should fail with 409) +- [ ] Test expired state token (wait 5+ minutes before callback) +- [ ] Test invalid state parameter (manually edit callback URL) + +### Security Testing +- [ ] Cannot link identity without being logged in +- [ ] Cannot reuse authorization code (one-time use) +- [ ] State token validated and deleted after use +- [ ] Provider account can't be linked to multiple AeThex accounts +- [ ] Server fetches identity (client can't spoof external_id) + +### Edge Cases +- [ ] User closes browser during OAuth flow +- [ ] Network error during token exchange +- [ ] Provider API returns invalid response +- [ ] User denies authorization on provider site + +--- + +## ๐Ÿ› Troubleshooting + +### "Invalid redirect_uri" +**Cause:** OAuth app redirect URI doesn't match exactly +**Fix:** +1. Check `.env` has correct `NODE_ENV` value +2. Verify OAuth app has correct URI registered +3. Ensure no trailing slash differences + +### "Invalid state" +**Cause:** State token expired (5 min) or browser started new session +**Fix:** +1. Start OAuth flow again +2. Complete within 5 minutes +3. Don't open multiple OAuth flows in parallel + +### "Identity already linked" +**Cause:** Provider account linked to different AeThex account +**Fix:** +1. User must log in to original AeThex account +2. Unlink identity from settings (TODO: implement unlink endpoint) +3. Try linking again from new account + +### Build errors +**Cause:** Missing type declarations or import paths +**Fix:** +1. Run `npm install` to ensure all dependencies installed +2. Check TypeScript errors: `npx tsc --noEmit` +3. Verify import paths use relative paths (not `@/` aliases in server) + +--- + +## ๐Ÿšง TODO / Future Improvements + +### High Priority +- [ ] Implement unlink endpoint: `DELETE /api/oauth/unlink/:provider` +- [ ] Add frontend UI for identity linking (Settings page) +- [ ] Redis/database for state storage (replace in-memory Map) +- [ ] Rate limiting on OAuth endpoints +- [ ] Logging/monitoring for OAuth events + +### Medium Priority +- [ ] Refresh token support (for long-lived access) +- [ ] Scope customization per provider +- [ ] Additional providers (Twitter/X, Google, Steam) +- [ ] Admin panel to view linked identities +- [ ] Webhook for identity verification events + +### Low Priority +- [ ] OAuth 2.1 compatibility +- [ ] Multiple identities per provider (e.g., 2 Discord accounts) +- [ ] Identity verification challenges +- [ ] Automated credential rotation reminders + +--- + +## ๐Ÿ“š References + +- [Discord OAuth2 Docs](https://discord.com/developers/docs/topics/oauth2) +- [Roblox OAuth 2.0 Guide](https://create.roblox.com/docs/cloud/open-cloud/oauth2-overview) +- [GitHub OAuth Apps](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps) +- [OAuth 2.0 RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) +- [PKCE RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) + +--- + +**Implemented:** December 24, 2025 +**Domain:** aethex.app +**Status:** โœ… Ready for testing (requires OAuth app registration) diff --git a/docs/OAUTH_QUICKSTART.md b/docs/OAUTH_QUICKSTART.md new file mode 100644 index 0000000..058fe6a --- /dev/null +++ b/docs/OAUTH_QUICKSTART.md @@ -0,0 +1,140 @@ +# OAuth Quick Start Guide + +## ๐Ÿš€ Get OAuth Working in 5 Minutes + +### Step 1: Register OAuth Apps (5 min per provider) + +#### Discord +1. Go to https://discord.com/developers/applications +2. Click "New Application" โ†’ Name it "AeThex OS Dev" +3. Go to **OAuth2** โ†’ Add redirect URI: + ``` + http://localhost:5000/api/oauth/callback/discord + ``` +4. Copy **Client ID** and **Client Secret** to `.env` + +#### Roblox +1. Go to https://create.roblox.com/dashboard/credentials +2. Create new **OAuth 2.0** credential +3. Add redirect URI: + ``` + http://localhost:5000/api/oauth/callback/roblox + ``` +4. Select scopes: `openid`, `profile` +5. Copy **Client ID** and **Client Secret** to `.env` + +#### GitHub +1. Go to https://github.com/settings/developers +2. Click **OAuth Apps** โ†’ **New OAuth App** +3. Fill in: + - Name: `AeThex OS Dev` + - Homepage: `http://localhost:5000` + - Callback URL: `http://localhost:5000/api/oauth/callback/github` +4. Copy **Client ID** and **Client Secret** to `.env` + +### Step 2: Verify Environment Variables + +Your `.env` should have: +```bash +DISCORD_CLIENT_ID=your_discord_client_id +DISCORD_CLIENT_SECRET=your_discord_client_secret +ROBLOX_CLIENT_ID=your_roblox_client_id +ROBLOX_CLIENT_SECRET=your_roblox_client_secret +GITHUB_CLIENT_ID=your_github_client_id +GITHUB_CLIENT_SECRET=your_github_client_secret +``` + +### Step 3: Test the OAuth Flow + +```bash +# Start server +npm run dev + +# In browser: +# 1. Log in to AeThex OS +# 2. Open browser console +# 3. Run this code: +fetch('/api/oauth/link/discord', { + method: 'POST', + credentials: 'include' +}) + .then(r => r.json()) + .then(data => { + console.log('Auth URL:', data.authUrl); + window.location.href = data.authUrl; // Redirects to Discord + }); + +# 4. Authorize on Discord +# 5. You'll be redirected back to /settings?oauth=success&provider=discord +``` + +### Step 4: Verify Database + +```sql +-- Check if identity was created +SELECT * FROM aethex_subject_identities +ORDER BY created_at DESC +LIMIT 1; +``` + +--- + +## ๐ŸŽฏ Next Steps + +### Add Frontend UI +Create a button in Settings page: + +```tsx +// client/src/pages/settings.tsx +async function linkDiscord() { + const res = await fetch('/api/oauth/link/discord', { + method: 'POST', + credentials: 'include' + }); + const { authUrl } = await res.json(); + window.location.href = authUrl; +} + +// Check for success on page load +useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.get('oauth') === 'success') { + toast.success(`${params.get('provider')} account linked!`); + } +}, []); +``` + +### For Production (aethex.app) +1. Create production OAuth apps with redirect URI: + ``` + https://aethex.app/api/oauth/callback/{provider} + ``` +2. Add production credentials to production `.env` +3. Set `NODE_ENV=production` +4. Deploy! + +--- + +## โš ๏ธ Security Reminders + +**Before Production:** +1. โœ… Rotate ALL credentials (see `docs/CREDENTIALS_ROTATION.md`) +2. โœ… Use separate OAuth apps for dev/prod +3. โœ… Ensure `.env` is in `.gitignore` +4. โœ… Enable HTTPS in production +5. โœ… Replace in-memory state storage with Redis + +--- + +## ๐Ÿ“ž Need Help? + +- **Setup issues:** See `docs/OAUTH_SETUP.md` +- **Security questions:** See `docs/CREDENTIALS_ROTATION.md` +- **Implementation details:** See `docs/OAUTH_IMPLEMENTATION.md` +- **Testing OAuth:** See "Testing Checklist" in implementation doc + +--- + +**Status:** โœ… OAuth handler implemented and ready for testing +**Build:** โœ… Compiles successfully +**Next:** Register OAuth apps and test the flow! diff --git a/docs/OAUTH_SETUP.md b/docs/OAUTH_SETUP.md new file mode 100644 index 0000000..1bade10 --- /dev/null +++ b/docs/OAUTH_SETUP.md @@ -0,0 +1,261 @@ +# OAuth Provider Setup Guide + +## Overview +AeThex OS uses OAuth 2.0 to link external platform identities (Discord, Roblox, GitHub) to user accounts. This guide explains how to configure OAuth applications for each provider. + +--- + +## ๐Ÿ”— Redirect URIs + +### Development Environment +When running locally, use these redirect URIs: + +- **Discord:** `http://localhost:5000/api/oauth/callback/discord` +- **Roblox:** `http://localhost:5000/api/oauth/callback/roblox` +- **GitHub:** `http://localhost:5000/api/oauth/callback/github` + +### Production Environment +For the live site at `aethex.app`, use: + +- **Discord:** `https://aethex.app/api/oauth/callback/discord` +- **Roblox:** `https://aethex.app/api/oauth/callback/roblox` +- **GitHub:** `https://aethex.app/api/oauth/callback/github` + +--- + +## ๐ŸŽฎ Discord OAuth App Setup + +### 1. Create OAuth Application +1. Go to [Discord Developer Portal](https://discord.com/developers/applications) +2. Click "New Application" and name it (e.g., "AeThex OS") +3. Navigate to **OAuth2** in the left sidebar + +### 2. Configure Redirects +Add these redirect URIs: +``` +http://localhost:5000/api/oauth/callback/discord +https://aethex.app/api/oauth/callback/discord +``` + +### 3. Configure Scopes +Required OAuth2 scopes: +- `identify` - Access user ID and username +- `email` - Access user email (optional but recommended) + +### 4. Get Credentials +Copy from the OAuth2 page: +- **Client ID** โ†’ `DISCORD_CLIENT_ID` +- **Client Secret** โ†’ `DISCORD_CLIENT_SECRET` + +Get from the **General Information** page: +- **Public Key** โ†’ `DISCORD_PUBLIC_KEY` + +### 5. Bot Token (Optional) +If using Discord bot features, go to **Bot** section: +- **Token** โ†’ `DISCORD_BOT_TOKEN` + +--- + +## ๐ŸŽฒ Roblox OAuth App Setup + +### 1. Create OAuth Application +1. Go to [Roblox Creator Dashboard](https://create.roblox.com/dashboard/credentials) +2. Create a new **OAuth 2.0** credential +3. Select "Read" access to user profile information + +### 2. Configure Redirects +Add these redirect URIs: +``` +http://localhost:5000/api/oauth/callback/roblox +https://aethex.app/api/oauth/callback/roblox +``` + +### 3. Configure Scopes +Required scopes: +- `openid` - OpenID Connect authentication +- `profile` - Access to profile information + +### 4. Get Credentials +Copy from credentials page: +- **Client ID** โ†’ `ROBLOX_CLIENT_ID` +- **Client Secret** โ†’ `ROBLOX_CLIENT_SECRET` + +### 5. Open Cloud API Key (Optional) +For server-to-server API calls: +1. Go to [Open Cloud](https://create.roblox.com/dashboard/credentials) +2. Create new API key with required permissions +3. Copy key โ†’ `ROBLOX_OPEN_CLOUD_API_KEY` + +--- + +## ๐Ÿ™ GitHub OAuth App Setup + +### 1. Create OAuth Application +1. Go to [GitHub Developer Settings](https://github.com/settings/developers) +2. Click **OAuth Apps** โ†’ **New OAuth App** +3. Fill in application details: + - **Application name:** AeThex OS + - **Homepage URL:** `https://aethex.app` + - **Authorization callback URL:** (see below) + +### 2. Configure Redirect URI +Use ONE of these (GitHub only allows one per app): + +**For Development:** +``` +http://localhost:5000/api/oauth/callback/github +``` + +**For Production:** +``` +https://aethex.app/api/oauth/callback/github +``` + +**๐Ÿ’ก Best Practice:** Create TWO separate OAuth apps: +- `AeThex OS (Development)` for localhost +- `AeThex OS` for production + +### 3. Get Credentials +Copy from OAuth app page: +- **Client ID** โ†’ `GITHUB_CLIENT_ID` +- **Client Secret** โ†’ `GITHUB_CLIENT_SECRET` + +### 4. Personal Access Token (Optional) +For server-to-server API calls: +1. Go to [Personal Access Tokens](https://github.com/settings/tokens) +2. Generate new token (classic) +3. Select required scopes (e.g., `read:user`) +4. Copy token โ†’ `GITHUB_PERSONAL_ACCESS_TOKEN` + +--- + +## ๐Ÿ’ณ Stripe Webhook Setup + +### 1. Get API Keys +1. Go to [Stripe Dashboard](https://dashboard.stripe.com/apikeys) +2. Copy keys: + - **Secret key** โ†’ `STRIPE_SECRET_KEY` + +### 2. Configure Webhooks +1. Go to **Developers** โ†’ **Webhooks** +2. Click **Add endpoint** +3. Set endpoint URL: + - Dev: `http://localhost:5000/api/webhooks/stripe` + - Prod: `https://aethex.app/api/webhooks/stripe` +4. Select events to listen for +5. Copy **Signing secret** โ†’ `STRIPE_WEBHOOK_SECRET` + +--- + +## ๐Ÿ” Environment Variables + +Add all credentials to `.env` file: + +```bash +# Discord OAuth +DISCORD_CLIENT_ID=your_client_id +DISCORD_CLIENT_SECRET=your_client_secret +DISCORD_PUBLIC_KEY=your_public_key +DISCORD_BOT_TOKEN=your_bot_token + +# GitHub OAuth +GITHUB_CLIENT_ID=your_client_id +GITHUB_CLIENT_SECRET=your_client_secret +GITHUB_PERSONAL_ACCESS_TOKEN=your_pat + +# Roblox OAuth +ROBLOX_CLIENT_ID=your_client_id +ROBLOX_CLIENT_SECRET=your_client_secret +ROBLOX_OPEN_CLOUD_API_KEY=your_api_key + +# Stripe +STRIPE_SECRET_KEY=your_secret_key +STRIPE_WEBHOOK_SECRET=your_webhook_secret +``` + +--- + +## โœ… Testing OAuth Flow + +### 1. Start Development Server +```bash +npm run dev +``` + +### 2. Test Identity Linking +1. Log in to AeThex OS +2. Go to **Settings** page +3. Click "Link Discord" (or other provider) +4. Authorize on provider's page +5. Verify redirect back to AeThex OS +6. Check database for new `subject_identities` entry + +### 3. Verify Security +- โœ… State parameter validated +- โœ… PKCE challenge verified (Roblox) +- โœ… Identity fetched server-side (not trusted from client) +- โœ… Duplicate identity detection working + +--- + +## ๐Ÿšจ Security Checklist + +- [ ] Redirect URIs match exactly (trailing slash matters!) +- [ ] Client secrets stored in `.env`, never committed to git +- [ ] State tokens expire after 5 minutes +- [ ] HTTPS enforced in production +- [ ] PKCE used for Roblox OAuth +- [ ] Server-side identity verification (no client-provided IDs) +- [ ] Duplicate identity linking prevented +- [ ] Error messages don't leak sensitive info + +--- + +## ๐Ÿ”„ Multi-Environment Strategy + +### Option 1: Environment-Specific Apps (Recommended) +Create separate OAuth apps for each environment: +- `AeThex OS Dev` โ†’ localhost redirects +- `AeThex OS Staging` โ†’ staging.aethex.app redirects +- `AeThex OS` โ†’ aethex.app redirects + +Use different `.env` files for each environment. + +### Option 2: Multiple Redirect URIs +Register all redirect URIs in a single app: +- Most providers allow multiple redirect URIs +- GitHub only allows one (requires separate apps) +- Use environment variables to select correct URI at runtime + +--- + +## ๐Ÿ“ž Support Links + +- **Discord Developer Portal:** https://discord.com/developers/applications +- **Roblox Creator Dashboard:** https://create.roblox.com/dashboard/credentials +- **GitHub Developer Settings:** https://github.com/settings/developers +- **Stripe Dashboard:** https://dashboard.stripe.com/ + +--- + +## ๐Ÿ› Troubleshooting + +### "Invalid redirect_uri" error +- Verify URI matches EXACTLY (no trailing slash difference) +- Check environment variable is set correctly +- Ensure OAuth app has URI registered + +### "Invalid state" error +- State token expired (5 min limit) +- User started flow in different session +- Clear browser cache and try again + +### "Identity already linked" error +- Provider account linked to different AeThex account +- User must unlink from original account first +- Check `subject_identities` table for conflicts + +### Token exchange fails +- Verify client secret is correct +- Check provider's API status page +- Ensure code hasn't expired (1-time use, 10 min limit) diff --git a/server/oauth-handlers.ts b/server/oauth-handlers.ts new file mode 100644 index 0000000..6477d2e --- /dev/null +++ b/server/oauth-handlers.ts @@ -0,0 +1,368 @@ +import type { Request, Response } from "express"; +import { supabase } from "./supabase"; + +// Extend Express Request type to include user +declare global { + namespace Express { + interface Request { + user?: { + id: string; + email?: string; + }; + } + } +} + +// OAuth State Management (in-memory for now, use Redis in production) +const oauthStates = new Map(); + +// Clean up expired states (5 min TTL) +setInterval(() => { + const now = Date.now(); + const keysToDelete: string[] = []; + + oauthStates.forEach((data, state) => { + if (now - data.createdAt > 300000) { + keysToDelete.push(state); + } + }); + + keysToDelete.forEach(key => oauthStates.delete(key)); +}, 60000); + +/** + * Start OAuth linking flow + * Client calls this to get authorization URL + */ +export async function startOAuthLinking(req: Request, res: Response) { + const { provider } = req.params; + const userId = req.session?.userId; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + if (!["discord", "roblox", "github"].includes(provider)) { + return res.status(400).json({ error: "Invalid provider" }); + } + + // Generate state token + const state = crypto.randomUUID(); + const codeVerifier = generateCodeVerifier(); + const codeChallenge = await generateCodeChallenge(codeVerifier); + + // Store state + oauthStates.set(state, { + userId, + provider, + codeVerifier, + createdAt: Date.now() + }); + + // Build authorization URL + const redirectUri = getRedirectUri(provider); + const authUrl = buildAuthorizationUrl(provider, state, codeChallenge, redirectUri); + + res.json({ authUrl, state }); +} + +/** + * OAuth callback handler + * Provider redirects here with authorization code + */ +export async function handleOAuthCallback(req: Request, res: Response) { + const { provider } = req.params; + const { code, state } = req.query; + + if (!code || !state || typeof state !== "string") { + return res.status(400).send("Invalid callback parameters"); + } + + // Validate state + const stateData = oauthStates.get(state); + if (!stateData || stateData.provider !== provider) { + return res.status(400).send("Invalid or expired state"); + } + + oauthStates.delete(state); + + try { + // Exchange code for token + const tokenData = await exchangeCodeForToken( + provider, + code as string, + getRedirectUri(provider), + stateData.codeVerifier + ); + + // Fetch user identity from provider + const identity = await fetchProviderIdentity(provider, tokenData.access_token); + + // Find or create subject + const { data: existingSubject } = await supabase + .from("aethex_subjects") + .select("*") + .eq("supabase_user_id", stateData.userId) + .single(); + + let subjectId: string; + + if (!existingSubject) { + const { data: newSubject, error: createError } = await supabase + .from("aethex_subjects") + .insert({ supabase_user_id: stateData.userId }) + .select() + .single(); + + if (createError) throw createError; + subjectId = newSubject.id; + } else { + subjectId = existingSubject.id; + } + + // Check if identity already exists + const { data: existingIdentity } = await supabase + .from("aethex_subject_identities") + .select("*") + .eq("issuer", provider) + .eq("external_id", identity.id) + .single(); + + if (existingIdentity && existingIdentity.subject_id !== subjectId) { + return res.status(409).send( + `This ${provider} account is already linked to another AeThex account.` + ); + } + + if (!existingIdentity) { + // Create new identity link + await supabase + .from("aethex_subject_identities") + .insert({ + subject_id: subjectId, + issuer: provider, + external_id: identity.id, + external_username: identity.username, + verified: true, + metadata: identity.metadata + }); + } + + // Redirect to success page + res.redirect(`${getAppBaseUrl()}/settings?oauth=success&provider=${provider}`); + } catch (error) { + console.error("OAuth callback error:", error); + res.redirect(`${getAppBaseUrl()}/settings?oauth=error&provider=${provider}`); + } +} + +/** + * Exchange authorization code for access token + */ +async function exchangeCodeForToken( + provider: string, + code: string, + redirectUri: string, + codeVerifier?: string +): Promise<{ access_token: string; token_type: string }> { + const config = getProviderConfig(provider); + + const params = new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: redirectUri, + client_id: config.clientId, + client_secret: config.clientSecret, + }); + + if (codeVerifier && provider === "roblox") { + params.append("code_verifier", codeVerifier); + } + + const response = await fetch(config.tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json" + }, + body: params + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + return response.json(); +} + +/** + * Fetch user identity from provider + */ +async function fetchProviderIdentity( + provider: string, + accessToken: string +): Promise<{ id: string; username: string; metadata: any }> { + const config = getProviderConfig(provider); + + const response = await fetch(config.userInfoUrl, { + headers: { + "Authorization": `Bearer ${accessToken}`, + "Accept": "application/json" + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch ${provider} user info`); + } + + const data = await response.json(); + + // Map provider-specific response to our format + switch (provider) { + case "discord": + return { + id: data.id, + username: `${data.username}#${data.discriminator}`, + metadata: { + avatar: data.avatar, + email: data.email, + verified: data.verified + } + }; + + case "roblox": + return { + id: data.sub, + username: data.preferred_username || data.name, + metadata: { + profile: data.profile, + picture: data.picture + } + }; + + case "github": + return { + id: String(data.id), + username: data.login, + metadata: { + name: data.name, + email: data.email, + avatar_url: data.avatar_url, + html_url: data.html_url + } + }; + + default: + throw new Error(`Unknown provider: ${provider}`); + } +} + +/** + * Build OAuth authorization URL + */ +function buildAuthorizationUrl( + provider: string, + state: string, + codeChallenge: string, + redirectUri: string +): string { + const config = getProviderConfig(provider); + const params = new URLSearchParams({ + client_id: config.clientId, + redirect_uri: redirectUri, + response_type: "code", + state, + scope: config.scope + }); + + if (provider === "roblox") { + params.append("code_challenge", codeChallenge); + params.append("code_challenge_method", "S256"); + } + + return `${config.authUrl}?${params}`; +} + +/** + * Get provider configuration + */ +function getProviderConfig(provider: string) { + const configs = { + discord: { + clientId: process.env.DISCORD_CLIENT_ID!, + clientSecret: process.env.DISCORD_CLIENT_SECRET!, + authUrl: "https://discord.com/api/oauth2/authorize", + tokenUrl: "https://discord.com/api/oauth2/token", + userInfoUrl: "https://discord.com/api/users/@me", + scope: "identify email" + }, + roblox: { + clientId: process.env.ROBLOX_CLIENT_ID!, + clientSecret: process.env.ROBLOX_CLIENT_SECRET!, + authUrl: "https://apis.roblox.com/oauth/v1/authorize", + tokenUrl: "https://apis.roblox.com/oauth/v1/token", + userInfoUrl: "https://apis.roblox.com/oauth/v1/userinfo", + scope: "openid profile" + }, + github: { + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + authUrl: "https://github.com/login/oauth/authorize", + tokenUrl: "https://github.com/login/oauth/access_token", + userInfoUrl: "https://api.github.com/user", + scope: "read:user user:email" + } + }; + + return configs[provider as keyof typeof configs]; +} + +/** + * Get OAuth redirect URI + */ +function getRedirectUri(provider: string): string { + const baseUrl = process.env.NODE_ENV === "production" + ? "https://aethex.app" + : `http://localhost:${process.env.PORT || 5000}`; + + return `${baseUrl}/api/oauth/callback/${provider}`; +} + +/** + * Get app base URL + */ +function getAppBaseUrl(): string { + return process.env.NODE_ENV === "production" + ? "https://aethex.app" + : `http://localhost:${process.env.PORT || 5000}`; +} + +/** + * PKCE helpers + */ +function generateCodeVerifier(): string { + const array = new Uint8Array(32); + crypto.getRandomValues(array); + return base64UrlEncode(array); +} + +async function generateCodeChallenge(verifier: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(verifier); + const hash = await crypto.subtle.digest("SHA-256", data); + return base64UrlEncode(new Uint8Array(hash)); +} + +function base64UrlEncode(array: Uint8Array): string { + return Buffer.from(array) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +} diff --git a/server/routes.ts b/server/routes.ts index 0d7ec1d..8e6a1d4 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -5,6 +5,7 @@ import { loginSchema, signupSchema } from "../shared/schema.js"; import { supabase } from "./supabase.js"; import { getChatResponse } from "./openai.js"; import { capabilityGuard } from "./capability-guard.js"; +import { startOAuthLinking, handleOAuthCallback } from "./oauth-handlers.js"; // Extend session type declare module 'express-session' { @@ -44,6 +45,14 @@ export async function registerRoutes( app.use("/api/os/entitlements/*", capabilityGuard); app.use("/api/os/link/*", capabilityGuard); + // ========== OAUTH ROUTES ========== + + // Start OAuth linking flow (get authorization URL) + app.post("/api/oauth/link/:provider", requireAuth, startOAuthLinking); + + // OAuth callback (provider redirects here with code) + app.get("/api/oauth/callback/:provider", handleOAuthCallback); + // ========== MODE MANAGEMENT ROUTES ========== // Get user mode preference