627 lines
15 KiB
Markdown
627 lines
15 KiB
Markdown
# Foundation OAuth Implementation - Complete Guide
|
|
|
|
## Overview
|
|
|
|
This guide details the **Phase 3 Switchover** implementation where aethex.dev becomes an OAuth **client** of aethex.foundation. aethex.foundation is the authoritative identity provider (the "issuer" of the Passport).
|
|
|
|
**Status:** ✅ Implementation complete with PKCE support
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
User Flow:
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ │
|
|
│ 1. User visits aethex.dev/login │
|
|
│ 2. Clicks "Login with Foundation" │
|
|
│ 3. Redirected to aethex.foundation /api/oauth/authorize │
|
|
│ 4. User authenticates on Foundation │
|
|
│ 5. Foundation redirects back to aethex.dev/auth/callback │
|
|
│ 6. Backend exchanges code for access token │
|
|
│ 7. User profile synced from Foundation to Corp DB │
|
|
│ 8. Session established on aethex.dev │
|
|
│ 9. Redirected to dashboard │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Foundation OAuth Credentials
|
|
|
|
These credentials were obtained during Foundation Phase 1 setup:
|
|
|
|
```
|
|
Foundation URL: https://aethex.foundation
|
|
Client ID: aethex_corp
|
|
Client Secret: bcoEtyQVGr6Z4557658eUXpDF5FDni2TGNahH3HT-FtylNrLCYwydwLO0sbKVHtfYUnZc4flAODa4BXkzxD_qg
|
|
Scopes: openid profile email achievements projects
|
|
```
|
|
|
|
**Environment Variables (add to .env):**
|
|
|
|
```bash
|
|
# Foundation identity provider
|
|
VITE_FOUNDATION_URL=https://aethex.foundation
|
|
|
|
# OAuth client credentials
|
|
FOUNDATION_OAUTH_CLIENT_ID=aethex_corp
|
|
FOUNDATION_OAUTH_CLIENT_SECRET=bcoEtyQVGr6Z4557658eUXpDF5FDni2TGNahH3HT-FtylNrLCYwydwLO0sbKVHtfYUnZc4flAODa4BXkzxD_qg
|
|
```
|
|
|
|
---
|
|
|
|
## Foundation OAuth Endpoints
|
|
|
|
### 1. Authorization Endpoint
|
|
|
|
Redirect users here to initiate authentication.
|
|
|
|
```
|
|
GET https://aethex.foundation/api/oauth/authorize
|
|
?client_id=aethex_corp
|
|
&redirect_uri=https://aethex.dev/auth/callback
|
|
&response_type=code
|
|
&scope=openid profile email achievements projects
|
|
&state=<csrf_token>
|
|
&code_challenge=<pkce_challenge>
|
|
&code_challenge_method=S256
|
|
```
|
|
|
|
**Parameters:**
|
|
|
|
- `client_id` - aethex_corp (identifies this app)
|
|
- `redirect_uri` - Where Foundation redirects after auth
|
|
- `response_type` - Always "code" (OAuth 2.0 authorization code flow)
|
|
- `scope` - Requested permissions
|
|
- `state` - CSRF token (generated by client, validated on callback)
|
|
- `code_challenge` - PKCE challenge (SHA256 hash of verifier)
|
|
- `code_challenge_method` - S256 (SHA256 PKCE method)
|
|
|
|
**Implementation:**
|
|
See `code/client/lib/foundation-oauth.ts` → `initiateFoundationLogin()`
|
|
|
|
### 2. Token Exchange Endpoint
|
|
|
|
Backend exchanges authorization code for access token.
|
|
|
|
```
|
|
POST https://aethex.foundation/api/oauth/token
|
|
Content-Type: application/x-www-form-urlencoded
|
|
|
|
grant_type=authorization_code
|
|
&code=<auth_code_from_callback>
|
|
&redirect_uri=https://aethex.dev/auth/callback
|
|
&client_id=aethex_corp
|
|
&client_secret=<stored_securely_on_backend>
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"access_token": "eyJ...",
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600,
|
|
"scope": "openid profile email achievements projects"
|
|
}
|
|
```
|
|
|
|
**Implementation:**
|
|
See `code/api/auth/callback.ts` → `performTokenExchange()`
|
|
|
|
### 3. User Info Endpoint
|
|
|
|
Fetch authenticated user's profile.
|
|
|
|
```
|
|
GET https://aethex.foundation/api/oauth/userinfo
|
|
Authorization: Bearer <access_token>
|
|
|
|
Response:
|
|
{
|
|
"id": "uuid",
|
|
"email": "user@example.com",
|
|
"username": "username",
|
|
"full_name": "Full Name",
|
|
"avatar_url": "https://...",
|
|
"profile_complete": true,
|
|
"achievements": ["achievement_id"],
|
|
"projects": ["project_id"]
|
|
}
|
|
```
|
|
|
|
**Implementation:**
|
|
See `code/api/auth/callback.ts` → `fetchUserInfoFromFoundation()`
|
|
|
|
---
|
|
|
|
## PKCE Implementation
|
|
|
|
**PKCE** (Proof Key for Code Exchange) adds extra security to OAuth flows.
|
|
|
|
### How PKCE Works
|
|
|
|
1. **Client generates code verifier:**
|
|
|
|
```javascript
|
|
verifier = randomString(64 chars, URL-safe)
|
|
// Example: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
|
```
|
|
|
|
2. **Client generates code challenge:**
|
|
|
|
```javascript
|
|
challenge = base64url(SHA256(verifier));
|
|
```
|
|
|
|
3. **Client sends challenge with authorization request:**
|
|
|
|
```
|
|
GET /api/oauth/authorize?...&code_challenge=...&code_challenge_method=S256
|
|
```
|
|
|
|
4. **Server stores challenge, validates with verifier on token exchange:**
|
|
|
|
```
|
|
POST /api/oauth/token?...&code_verifier=...
|
|
```
|
|
|
|
5. **Server verifies:**
|
|
```
|
|
base64url(SHA256(submitted_verifier)) === stored_challenge
|
|
```
|
|
|
|
**Why PKCE?**
|
|
|
|
- Prevents authorization code interception attacks
|
|
- Secure for mobile apps and single-page applications
|
|
- Required by OAuth 2.1 best practices
|
|
|
|
**Implementation:**
|
|
See `code/client/lib/foundation-oauth.ts` → `generatePKCEParams()`
|
|
|
|
---
|
|
|
|
## Implementation Details
|
|
|
|
### Frontend Components
|
|
|
|
#### 1. Login Page (`code/client/pages/Login.tsx`)
|
|
|
|
Updated to show Foundation OAuth button:
|
|
|
|
```typescript
|
|
<Button onClick={() => initiateFoundationLogin()}>
|
|
<Shield /> Login with Foundation
|
|
</Button>
|
|
```
|
|
|
|
Features:
|
|
|
|
- Initiates Foundation OAuth flow
|
|
- Generates PKCE parameters
|
|
- Stores verifier and state in sessionStorage
|
|
- Redirects to Foundation authorization endpoint
|
|
|
|
#### 2. Foundation OAuth Library (`code/client/lib/foundation-oauth.ts`)
|
|
|
|
Core OAuth functionality:
|
|
|
|
```typescript
|
|
// Generate PKCE parameters
|
|
async function generatePKCEParams(): Promise<{ verifier; challenge }>;
|
|
|
|
// Build authorization URL
|
|
async function getFoundationAuthorizationUrl(options?): Promise<string>;
|
|
|
|
// Initiate login
|
|
async function initiateFoundationLogin(redirectTo?: string): Promise<void>;
|
|
|
|
// Exchange code for token (called from backend)
|
|
async function exchangeCodeForToken(code: string): Promise<TokenResponse>;
|
|
```
|
|
|
|
#### 3. useFoundationAuth Hook (`code/client/hooks/use-foundation-auth.ts`)
|
|
|
|
Handles OAuth callback:
|
|
|
|
```typescript
|
|
// Process OAuth callback in URL
|
|
useFoundationAuth(): UseFoundationAuthReturn
|
|
|
|
// Check authentication status
|
|
useFoundationAuthStatus(): { isAuthenticated, userId }
|
|
```
|
|
|
|
### Backend Endpoints
|
|
|
|
#### 1. Callback Handler (`code/api/auth/callback.ts`)
|
|
|
|
**Route:** `GET /auth/callback?code=...&state=...`
|
|
|
|
**Flow:**
|
|
|
|
1. Receive authorization code from Foundation
|
|
2. Validate state token (CSRF protection)
|
|
3. Exchange code for access token
|
|
4. Fetch user info from Foundation
|
|
5. Sync user to local database
|
|
6. Set session cookies
|
|
7. Redirect to dashboard
|
|
|
|
**Code:**
|
|
|
|
```typescript
|
|
async function handleCallback(req, res) {
|
|
// 1. Get code from URL
|
|
const { code, state } = req.query;
|
|
|
|
// 2. Validate state
|
|
validateState(state);
|
|
|
|
// 3. Exchange for token
|
|
const token = await performTokenExchange(code);
|
|
|
|
// 4. Fetch user info
|
|
const user = await fetchUserInfoFromFoundation(token);
|
|
|
|
// 5. Sync to database
|
|
await syncUserToLocalDatabase(user);
|
|
|
|
// 6. Set cookies
|
|
res.setHeader("Set-Cookie", [
|
|
`foundation_access_token=${token}; ...`,
|
|
`auth_user_id=${user.id}; ...`,
|
|
]);
|
|
|
|
// 7. Redirect
|
|
return res.redirect("/dashboard");
|
|
}
|
|
```
|
|
|
|
#### 2. Token Exchange (`POST /auth/callback/exchange`)
|
|
|
|
**Called from frontend** to exchange code for token safely:
|
|
|
|
```typescript
|
|
async function handleTokenExchange(req, res) {
|
|
const { code } = req.body;
|
|
|
|
// Exchange code with Foundation
|
|
// Fetch user info
|
|
// Sync to database
|
|
// Set cookies
|
|
// Return token + user data
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Session Management
|
|
|
|
### Session Cookies
|
|
|
|
After successful authentication:
|
|
|
|
```
|
|
Cookies set:
|
|
├── foundation_access_token: <jwt_token>
|
|
│ └── HttpOnly, Secure, SameSite=Strict
|
|
│ └── Max-Age: from expires_in
|
|
│
|
|
└── auth_user_id: <user_uuid>
|
|
└── Secure, SameSite=Strict
|
|
└── Max-Age: 30 days
|
|
```
|
|
|
|
### Using Access Token
|
|
|
|
For authenticated API requests:
|
|
|
|
```typescript
|
|
// Get token from cookie
|
|
const token = document.cookie
|
|
.split(";")
|
|
.find((c) => c.trim().startsWith("foundation_access_token="))
|
|
?.split("=")[1];
|
|
|
|
// Use in requests
|
|
fetch("/api/user/profile", {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
credentials: "include",
|
|
});
|
|
```
|
|
|
|
### Clearing Session
|
|
|
|
On logout:
|
|
|
|
```typescript
|
|
// Clear cookies
|
|
document.cookie =
|
|
"foundation_access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC;";
|
|
document.cookie = "auth_user_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC;";
|
|
|
|
// Optional: Notify Foundation of logout
|
|
await fetch(`${FOUNDATION_URL}/api/oauth/logout`, {
|
|
method: "POST",
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## User Profile Sync
|
|
|
|
### Sync Flow
|
|
|
|
```
|
|
Foundation User Data:
|
|
├── id: UUID
|
|
├── email: string
|
|
├── username: string
|
|
├── full_name: string
|
|
├── avatar_url: URL
|
|
├── profile_complete: boolean
|
|
└── achievements, projects: arrays
|
|
|
|
↓ (via /api/oauth/userinfo)
|
|
|
|
Corp Local Database (user_profiles table):
|
|
├── id: UUID (primary key)
|
|
├── email: string
|
|
├── username: string
|
|
├── full_name: string
|
|
├── avatar_url: string
|
|
├── profile_completed: boolean
|
|
├── updated_at: timestamp
|
|
└── (other corp-specific fields)
|
|
```
|
|
|
|
### Upsert Logic
|
|
|
|
```typescript
|
|
await supabase.from("user_profiles").upsert({
|
|
id: foundationUser.id, // Match by ID
|
|
email: foundationUser.email,
|
|
username: foundationUser.username,
|
|
full_name: foundationUser.full_name,
|
|
avatar_url: foundationUser.avatar_url,
|
|
profile_completed: foundationUser.profile_complete,
|
|
updated_at: new Date().toISOString(),
|
|
});
|
|
```
|
|
|
|
**Note:** Existing user data is preserved, Foundation data is merged/updated.
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### Local Testing
|
|
|
|
1. **Set up environment:**
|
|
|
|
```bash
|
|
export VITE_FOUNDATION_URL=http://localhost:3001 # Or staging URL
|
|
export FOUNDATION_OAUTH_CLIENT_ID=aethex_corp
|
|
export FOUNDATION_OAUTH_CLIENT_SECRET=<secret>
|
|
```
|
|
|
|
2. **Test flow:**
|
|
|
|
- Visit `http://localhost:5173/login`
|
|
- Click "Login with Foundation"
|
|
- Should redirect to Foundation auth page
|
|
- After auth, should return to aethex.dev/auth/callback with code
|
|
- Should exchange code and redirect to dashboard
|
|
- Check cookies: `foundation_access_token` and `auth_user_id`
|
|
|
|
3. **Verify database:**
|
|
```sql
|
|
SELECT id, email, username FROM user_profiles WHERE id = '<user_id>';
|
|
```
|
|
|
|
### Error Scenarios
|
|
|
|
**Invalid code:**
|
|
|
|
```
|
|
GET /auth/callback?code=invalid
|
|
→ 400 "Token exchange failed"
|
|
→ Redirect to /login?error=auth_failed
|
|
```
|
|
|
|
**Invalid state:**
|
|
|
|
```
|
|
GET /auth/callback?code=...&state=wrong_state
|
|
→ Error "Invalid state token - possible CSRF attack"
|
|
→ Redirect to /login?error=auth_failed
|
|
```
|
|
|
|
**Foundation down:**
|
|
|
|
```
|
|
POST /api/oauth/token → ECONNREFUSED
|
|
→ Error "Failed to exchange code"
|
|
→ Redirect to /login?error=auth_failed
|
|
```
|
|
|
|
---
|
|
|
|
## Files Modified/Created
|
|
|
|
### New Files
|
|
|
|
```
|
|
code/
|
|
├── client/
|
|
│ ├── lib/foundation-oauth.ts (PKCE + OAuth flow)
|
|
│ ├── lib/foundation-auth.ts (Token/profile management)
|
|
│ └── hooks/use-foundation-auth.ts (React hooks for callback)
|
|
├── api/
|
|
│ └── auth/callback.ts (OAuth callback handler)
|
|
└── .env.foundation-oauth.example (Configuration template)
|
|
```
|
|
|
|
### Modified Files
|
|
|
|
```
|
|
code/
|
|
└── client/
|
|
└── pages/Login.tsx (Added Foundation button)
|
|
```
|
|
|
|
### Deprecated Files
|
|
|
|
These can be removed after testing completes:
|
|
|
|
```
|
|
code/api/discord/oauth/start.ts
|
|
code/api/discord/oauth/callback.ts
|
|
code/api/discord/link.ts
|
|
code/api/discord/create-linking-session.ts
|
|
code/api/discord/verify-code.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Deployment Checklist
|
|
|
|
- [ ] Environment variables set in deployment platform
|
|
- [ ] `FOUNDATION_OAUTH_CLIENT_SECRET` stored securely (not in git)
|
|
- [ ] `/auth/callback` route properly configured
|
|
- [ ] HTTPS enabled (required for secure cookies)
|
|
- [ ] SameSite cookie policies enforced
|
|
- [ ] PKCE validation working on Foundation side
|
|
- [ ] User profile sync tested
|
|
- [ ] Session management tested
|
|
- [ ] Error handling tested
|
|
- [ ] Old Discord OAuth endpoints disabled (after rollout)
|
|
|
|
---
|
|
|
|
## Monitoring
|
|
|
|
### Key Metrics
|
|
|
|
1. **Auth Success Rate**
|
|
|
|
- Target: >99%
|
|
- Alert if: <95%
|
|
|
|
2. **Token Exchange Time**
|
|
|
|
- Target: <500ms
|
|
- Alert if: >2s
|
|
|
|
3. **Error Categories**
|
|
|
|
- Track: invalid_code, state_mismatch, timeout, etc.
|
|
|
|
4. **Foundation Connectivity**
|
|
- Monitor: /api/oauth/authorize, /api/oauth/token availability
|
|
- Alert on: persistent errors from Foundation
|
|
|
|
### Logging
|
|
|
|
Key points to log:
|
|
|
|
```
|
|
[Foundation OAuth] Step 1: Received authorization code
|
|
[Foundation OAuth] Step 2: State validation passed
|
|
[Foundation OAuth] Step 3: Token exchange initiated
|
|
[Foundation OAuth] Step 4: User info fetched
|
|
[Foundation OAuth] Step 5: User profile synced
|
|
[Foundation OAuth] Step 6: Session established
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Problem: "No authorization code received"
|
|
|
|
**Cause:** Foundation didn't redirect back properly
|
|
|
|
**Solutions:**
|
|
|
|
1. Check `redirect_uri` matches exactly (case-sensitive)
|
|
2. Verify Foundation OAuth settings
|
|
3. Check browser console for JavaScript errors
|
|
|
|
### Problem: "Invalid state token"
|
|
|
|
**Cause:** CSRF validation failed
|
|
|
|
**Solutions:**
|
|
|
|
1. Check that state is generated consistently
|
|
2. Verify sessionStorage isn't cleared between redirect
|
|
3. Check for multiple browser tabs (different state per tab)
|
|
|
|
### Problem: "Token exchange failed"
|
|
|
|
**Cause:** Foundation token endpoint unavailable or code invalid
|
|
|
|
**Solutions:**
|
|
|
|
1. Check Foundation is running and `/api/oauth/token` accessible
|
|
2. Verify `client_id` and `client_secret` are correct
|
|
3. Check code hasn't expired (usually 10 minutes)
|
|
4. Review Foundation logs for errors
|
|
|
|
### Problem: User not synced to database
|
|
|
|
**Cause:** Database error during sync
|
|
|
|
**Solutions:**
|
|
|
|
1. Check `user_profiles` table exists and has proper schema
|
|
2. Verify Supabase connection and permissions
|
|
3. Check user_id isn't duplicated in database
|
|
4. Review application logs for sync errors
|
|
|
|
---
|
|
|
|
## FAQ
|
|
|
|
**Q: Why PKCE?**
|
|
A: OAuth 2.1 best practice. Prevents interception attacks on the authorization code.
|
|
|
|
**Q: Can I use Foundation login on multiple apps?**
|
|
A: Yes! Any app with valid credentials can use Foundation for auth.
|
|
|
|
**Q: What if Foundation is down?**
|
|
A: Users cannot login. Have a maintenance page ready.
|
|
|
|
**Q: Do I need to handle token refresh?**
|
|
A: Foundation tokens are long-lived. Implement refresh if tokens are short-lived.
|
|
|
|
**Q: Can users logout from Foundation and still access Corp?**
|
|
A: Yes, they have separate sessions. Eventually their Corp token will expire.
|
|
|
|
**Q: What happens to existing Discord connections?**
|
|
A: They're managed by Foundation now, not Corp. Users reconnect on Foundation.
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
✅ **aethex.dev is now an OAuth client of aethex.foundation**
|
|
|
|
- **Foundation** = Single source of truth for identity (Discord, email, etc.)
|
|
- **aethex.dev** = OAuth client that redirects to Foundation for authentication
|
|
- **User experience** = Seamless login with unified account across ecosystem
|
|
- **Security** = PKCE prevents interception, secure cookie handling
|
|
- **Architecture** = Axiom Model achieved - Foundation controls all identities
|
|
|
|
---
|
|
|
|
**Status:** ✅ Implementation complete, ready for testing and deployment
|