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 statistics
  • GET /api/admin/audit-logs - Admin activity logs
  • PATCH /api/admin/users/:id/role - Change user roles
  • GET /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:


Admin Configuration

Setting Up Admins

  1. Get Discord User ID:

    • Enable Developer Mode: Settings → App Settings → Advanced → Developer Mode
    • Right-click your username → "Copy User ID"
  2. Add to Environment Variables:

    Development: ./.env.development

    ADMIN_DISCORD_IDS=444645441980858368
    

    Production: ./.env.production

    ADMIN_DISCORD_IDS=444645441980858368,987654321098765432
    

    Multiple admins: Separate with commas, no spaces

  3. Apply Changes:

    • Restart backend server
    • User must logout and login again for role to update

Testing Authentication

Manual Testing

  1. Start web panel:

    npm run dev:web
    
  2. Login:

    • Navigate to http://localhost:3000
    • Click "Login with Discord"
    • Authorize application
  3. 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

  1. Session Security:

    • ✅ HttpOnly cookies (prevents XSS)
    • ✅ SameSite=lax (prevents CSRF)
    • ✅ Secure flag in production (HTTPS only)
    • ✅ Automatic expiry (7 days)
    • ✅ Server-side session validation
  2. Token Management:

    • ✅ Cryptographically secure random tokens (32 bytes)
    • ✅ Tokens never exposed to client JavaScript
    • ✅ Automatic Discord token refresh
    • ✅ Expired session cleanup
  3. CSRF Protection:

  4. Rate Limiting:

  5. 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:

  1. Verify your Discord ID is in .env.developmentADMIN_DISCORD_IDS
  2. Restart backend: Stop and run npm run dev:web again
  3. Logout completely from the web panel
  4. Login again with Discord
  5. 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:

// 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:

  1. Check backend logs for errors
  2. 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
    
  3. Verify callback URL in Discord Developer Portal matches exactly
  4. Check database is accessible

Files Reference

Backend

Frontend

Database

Configuration

Testing


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/me returns user object directly)
  • Avatar URL field name fixed (user.id instead of user.discordId)
  • Role update on login (not just creation)
  • CSRF_SECRET added to env files

🎯 To Use Admin Panel:

  1. Add your Discord ID to ADMIN_DISCORD_IDS in .env.development
  2. Restart backend
  3. Logout and login via Discord
  4. Navigate to /admin