Authentication Flow
How authentication works in the system
Authentication Flow Documentation
Overview
The DBK Gaming web panel uses Discord OAuth2 for authentication with role-based access control (RBAC). Admin roles are assigned based on Discord IDs configured in environment variables.
Complete Authentication Flow
1. User Login (Discord OAuth2)
User clicks "Login with Discord"
↓
Frontend redirects to: /api/auth/login
↓
Backend redirects to: Discord OAuth2 authorization
↓
User authorizes application on Discord
↓
Discord redirects to: /api/auth/callback with authorization code
Files involved:
2. OAuth Callback & User Creation/Update
Location: ./web/backend/src/config/passport.js
// Discord OAuth callback
async (accessToken, refreshToken, profile, done) => {
// 1. Check if user's Discord ID is in ADMIN_DISCORD_IDS
const isAdmin = ADMIN_DISCORD_IDS.includes(profile.id);
const role = isAdmin ? 'ADMIN' : 'MEMBER';
// 2. Create or update user in database
const user = await prisma.user.upsert({
where: { id: profile.id },
update: {
username, avatar, accessToken, refreshToken, tokenExpiry,
role // ← IMPORTANT: Role updated on every login
},
create: { ...userData, role }
});
return done(null, user);
}
Key Points:
- ✅ Admin role is checked on every login against
ADMIN_DISCORD_IDS - ✅ User role is always updated during login (not just on creation)
- ✅ Discord tokens are stored for future API calls (guilds, user info)
- ✅ Token expiry is tracked for automatic refresh
Database Schema: ./prisma/schema.prisma
model User {
id String @id // Discord User ID
username String
avatar String?
role String @default("MEMBER") // ADMIN or MEMBER
accessToken String? // Discord OAuth token
refreshToken String? // For token refresh
tokenExpiry DateTime?
// ... other fields
}
3. Session Creation
Location: ./web/backend/src/routes/auth.js - /callback endpoint
// After successful OAuth
const sessionToken = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
await prisma.session.create({
data: {
userId: req.user.id,
token: sessionToken,
expiresAt
}
});
// Set httpOnly cookie
res.cookie('session_token', sessionToken, {
httpOnly: true, // Not accessible via JavaScript
secure: production, // HTTPS only in production
sameSite: 'lax', // CSRF protection
maxAge: 7 days,
path: '/'
});
// Redirect to frontend
res.redirect(process.env.FRONTEND_URL);
Security Features:
- ✅ Secure random token generation (32 bytes)
- ✅ HttpOnly cookies prevent XSS attacks
- ✅ SameSite protection against CSRF
- ✅ 7-day expiry for sessions
- ✅ Secure flag enabled in production (HTTPS)
4. Session Validation (Every Request)
Location: ./web/backend/src/middleware/auth.js
export const requireAuth = async (req, res, next) => {
const sessionToken = req.cookies.session_token;
// 1. Find session by token
const session = await prisma.session.findUnique({
where: { token: sessionToken },
include: { user: true }
});
// 2. Check expiry
if (session.expiresAt < new Date()) {
await prisma.session.delete({ where: { id: session.id } });
res.clearCookie('session_token');
return res.status(401).json({ error: 'Session expired' });
}
// 3. Refresh Discord token if needed
await refreshDiscordTokenIfNeeded(session.user);
// 4. Attach user to request
req.user = session.user;
next();
};
Token Refresh Logic:
- Discord tokens expire after 7 days
- Automatically refreshed when < 24 hours remaining
- Uses refresh token to get new access token
- Prevents need for re-authentication
5. Admin Access Control
Backend Middleware: ./web/backend/src/middleware/auth.js
export const requireAdmin = async (req, res, next) => {
// First authenticate user
if (!req.user) {
await requireAuth(req, res, () => {});
if (!req.user) return;
}
// Check admin role
if (req.user.role !== 'ADMIN') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
};
Frontend Protection: ./web/frontend/app/(protected)/admin/page.jsx
const { user, isAdmin } = useAuth();
useEffect(() => {
if (!authLoading) {
if (!user) {
router.push('/'); // Not logged in
} else if (!isAdmin()) {
router.push('/forbidden'); // Not an admin
}
}
}, [user, authLoading, isAdmin, router]);
Admin Routes Protected:
GET /api/admin/stats- Dashboard statisticsGET /api/admin/audit-logs- Admin activity logsPATCH /api/admin/users/:id/role- Change user rolesGET /api/admin/*- All admin endpoints
6. Frontend User State Management
Location: ./web/frontend/contexts/AuthContext.jsx
const fetchCurrentUser = async () => {
try {
const response = await api.get('/api/auth/me');
setUser(response.data); // ← User object with role
} catch (err) {
setUser(null);
}
};
const isAdmin = () => {
return user?.role === 'ADMIN';
};
API Endpoint: ./web/backend/src/routes/auth.js - /me
router.get('/me', async (req, res) => {
const session = await prisma.session.findUnique({
where: { token: req.cookies.session_token },
include: { user: true }
});
// Remove sensitive tokens before sending
const { accessToken, refreshToken, ...userWithoutTokens } = session.user;
res.json(userWithoutTokens); // ← Direct user object (not nested)
});
Available in Frontend:
const { user, isAuthenticated, isAdmin, login, logout } = useAuth();
// user object contains:
{
id: "444645441980858368",
username: "YourUsername",
avatar: "hash",
role: "ADMIN", // or "MEMBER"
bio: null,
favoriteGames: null,
socialLinks: null,
createdAt: "2024-...",
updatedAt: "2024-..."
}
7. Logout Flow
User clicks "Logout"
↓
Frontend calls: POST /api/auth/logout
↓
Backend deletes session from database
↓
Backend clears session_token cookie
↓
Frontend sets user to null
↓
Frontend redirects to home
Files involved:
- ./web/frontend/contexts/AuthContext.jsx -
logout() - ./web/backend/src/routes/auth.js -
/logoutendpoint
Admin Configuration
Setting Up Admins
-
Get Discord User ID:
- Enable Developer Mode: Settings → App Settings → Advanced → Developer Mode
- Right-click your username → "Copy User ID"
-
Add to Environment Variables:
Development: ./.env.development
ADMIN_DISCORD_IDS=444645441980858368Production: ./.env.production
ADMIN_DISCORD_IDS=444645441980858368,987654321098765432Multiple admins: Separate with commas, no spaces
-
Apply Changes:
- Restart backend server
- User must logout and login again for role to update
Testing Authentication
Manual Testing
-
Start web panel:
npm run dev:web -
Login:
- Navigate to
http://localhost:3000 - Click "Login with Discord"
- Authorize application
- Navigate to
-
Verify:
- Check user menu shows your username
- Check role badge shows "Administrator" (if admin)
- Check "Admin" link appears in navbar (if admin)
- Navigate to
/admin- should load dashboard (if admin)
Automated Testing
Run test script:
node scripts/test-auth-flow.js
Test output:
=== Testing Auth Flow & User Management ===
1. Admin Configuration:
ADMIN_DISCORD_IDS: 444645441980858368
✓ 1 admin ID(s) configured
2. User Table:
Total users: 1
Users:
✓ YourUsername (444645441980858368)
Role: ADMIN (Should be ADMIN)
Active sessions: 1
Created: 2/13/2024
3. Active Sessions:
Active sessions: 1
- YourUsername (ADMIN)
Expires: 2/20/2024, 10:00:00 AM
4. Admin Verification:
Users with ADMIN role: 1
Users in ADMIN_DISCORD_IDS: 1
✓ All user roles match configuration
5. Database Status:
✓ Database connection OK
✅ Auth flow looks good! Admin panel should be accessible.
Security Features
Implemented Security Measures
-
Session Security:
- ✅ HttpOnly cookies (prevents XSS)
- ✅ SameSite=lax (prevents CSRF)
- ✅ Secure flag in production (HTTPS only)
- ✅ Automatic expiry (7 days)
- ✅ Server-side session validation
-
Token Management:
- ✅ Cryptographically secure random tokens (32 bytes)
- ✅ Tokens never exposed to client JavaScript
- ✅ Automatic Discord token refresh
- ✅ Expired session cleanup
-
CSRF Protection:
- ✅ CSRF tokens for state-changing operations
- ✅ SameSite cookie attribute
- Middleware: ./web/backend/src/middleware/csrf.js
-
Rate Limiting:
- ✅ Auth endpoints: 10 requests/minute
- ✅ API endpoints: 60 requests/minute
- Middleware: ./web/backend/src/middleware/rateLimit.js
-
Access Control:
- ✅ Backend middleware validation
- ✅ Frontend route protection
- ✅ Role-based permissions
- ✅ Server owner validation
Common Issues & Solutions
Issue: "I'm logged in but can't access admin panel"
Cause: User was created/logged in before being added to ADMIN_DISCORD_IDS
Solution:
- Verify your Discord ID is in
.env.development→ADMIN_DISCORD_IDS - Restart backend: Stop and run
npm run dev:webagain - Logout completely from the web panel
- Login again with Discord
- Your role will be updated to ADMIN on login
Verify with test script:
node scripts/test-auth-flow.js
Issue: "Avatar not showing in user menu"
Cause: Bug using wrong field name (now fixed)
Fixed in: ./web/frontend/components/auth/UserMenu.jsx
// Before (wrong):
const getAvatarUrl = () => {
if (!user.discordId || !user.avatar) return null;
return `https://cdn.discordapp.com/avatars/${user.discordId}/${user.avatar}.png`;
};
// After (correct):
const getAvatarUrl = () => {
if (!user.id || !user.avatar) return null;
return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`;
};
Issue: "Session expired too quickly"
Current expiry: 7 days
To change: Edit both files:
- ./web/backend/src/routes/auth.js - Session creation
- ./web/backend/src/routes/auth.js - Cookie maxAge
// Example: 30 days
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
// ...
maxAge: 30 * 24 * 60 * 60 * 1000
Issue: "Can't login - redirects to error page"
Debug steps:
- Check backend logs for errors
- Verify Discord OAuth2 credentials:
DISCORD_CLIENT_ID=your_client_id DISCORD_CLIENT_SECRET=your_client_secret DISCORD_CALLBACK_URL=http://localhost:3001/api/auth/callback - Verify callback URL in Discord Developer Portal matches exactly
- Check database is accessible
Files Reference
Backend
- ./web/backend/src/config/passport.js - OAuth strategy
- ./web/backend/src/routes/auth.js - Auth endpoints
- ./web/backend/src/middleware/auth.js - Auth middleware
- ./web/backend/src/middleware/csrf.js - CSRF protection
- ./web/backend/src/middleware/rateLimit.js - Rate limiting
- ./web/backend/src/routes/admin.js - Admin endpoints
Frontend
- ./web/frontend/contexts/AuthContext.jsx - Auth state
- ./web/frontend/components/auth/LoginButton.jsx - Login UI
- ./web/frontend/components/auth/UserMenu.jsx - User dropdown
- ./web/frontend/components/layout/Navbar.jsx - Navigation
- ./web/frontend/app/(protected)/admin/page.jsx - Admin dashboard
Database
- ./prisma/schema.prisma - Schema definition
- Models:
User,Session
Configuration
- ./.env.development - Dev environment
- ./.env.production - Prod environment
- ./.env.example - Template
Testing
- ./scripts/test-auth-flow.js - Auth test script
- ./scripts/create-admin.js - Manual admin creation
Summary
✅ Auth Flow Working:
- Discord OAuth2 login functional
- Role assignment based on
ADMIN_DISCORD_IDS - Roles update on every login (not just creation)
- Sessions properly managed (7-day expiry)
- Admin panel access control working
✅ Security:
- HttpOnly, Secure, SameSite cookies
- CSRF protection enabled
- Rate limiting active
- Token refresh automatic
- Expired session cleanup
✅ Fixed Issues:
- API response structure corrected (
/api/auth/mereturns user object directly) - Avatar URL field name fixed (
user.idinstead ofuser.discordId) - Role update on login (not just creation)
- CSRF_SECRET added to env files
🎯 To Use Admin Panel:
- Add your Discord ID to
ADMIN_DISCORD_IDSin.env.development - Restart backend
- Logout and login via Discord
- Navigate to
/admin