Authentication System
The UiASub website includes an authentication system for internal tools, using Discord OAuth via Supabase for secure, role-based access control.
Overview
Internal tools like equipment management and storage tracking require authentication. The system leverages:
- Discord OAuth 2.0 for identity verification
- Supabase for backend authentication and database
- Supabase Edge Functions for serverless role verification
- localStorage for session persistence
Architecture
User → Login Page → Discord OAuth → Supabase Auth
↓
Supabase Edge Function
↓
Discord API (Role Check)
↓
Return Token + Status
↓
Client stores token in localStorage
Authentication Flow
1. User Initiates Login
Trigger: User clicks "Logg Inn" / "Log In" in the header
Action: Redirects to pages/login.html
2. Discord OAuth Redirect
Page: pages/login.html
Process:
- User clicks the Discord login button
- Supabase redirects to Discord OAuth
- User authorizes the application
- Discord redirects back to the site
3. Token Exchange & Verification
Script: js/login.js
Process:
// 1. Extract access token from URL
const token = supabase.auth.getSession()
// 2. Send token to Edge Function for role verification
const response = await fetch(edgeFunctionURL, {
headers: { Authorization: `Bearer ${token}` }
})
// 3. Edge Function checks Discord role
const hasRequiredRole = await checkDiscordRole(userId)
// 4. Return verification result
return { verified: hasRequiredRole, user: userData }
4. Session Storage
If verified:
- Token stored in
localStorage.setItem('sb-auth-token', token) - User redirected to requested internal page
- Session persists across page loads
If not verified:
- Error message displayed
- User remains on login page
- No token stored
5. Session Validation
On every page load (js/header.js):
// Check if token exists
const token = localStorage.getItem('sb-auth-token')
if (token) {
// Verify token is still valid (cached for 5 minutes)
const isValid = await verifyToken(token)
if (isValid) {
// Show "Logged In" state
} else {
// Clear invalid token and show "Log In"
localStorage.removeItem('sb-auth-token')
}
}
Key Files
Frontend
| File | Purpose |
|---|---|
pages/login.html |
Login page with Discord OAuth button |
js/login.js |
Handles OAuth flow and token verification |
js/header.js |
Checks auth status and updates UI |
js/equipment.js |
Authenticated equipment management |
js/storage.js |
Authenticated storage management |
Backend (Supabase)
| Resource | Purpose |
|---|---|
Edge Function: discord-role-sync |
Verifies Discord roles server-side |
Database: equipment table |
Stores equipment data |
Database: storage table |
Stores storage data |
| Auth: Discord OAuth provider | Handles user authentication |
Required Discord Roles
The Edge Function checks for specific Discord roles:
| Role Name | Access Level |
|---|---|
Member |
Read-only access to internal tools |
Leadership |
Full CRUD access to internal tools |
Note: Role names are configurable in the Edge Function.
Supabase Edge Function
Purpose
Server-side role verification to prevent client-side bypass.
Endpoint
Authentication
fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
})
Response
{
"verified": true,
"user": {
"id": "discord-user-id",
"username": "username#0000",
"roles": ["Member", "Leadership"]
}
}
Protected Pages
Equipment Management (pages/equipment.html)
Features:
- View equipment inventory
- Add new equipment
- Assign equipment to members
- Return equipment
- Delete equipment records
Access Control:
- Read: All authenticated members
- Write: Leadership only
Storage Management (pages/storage.html)
Features:
- View storage locations
- Add storage items
- Update item status
- Remove items
Access Control:
- Read: All authenticated members
- Write: Leadership only
Security Measures
Client-Side
- Token Storage:
localStorage(secure, but not HTTP-only) - Token Validation: Verified on every protected page load
- Timeout: Tokens expire after session timeout
- Cache: Auth status cached for 5 minutes to reduce API calls
Server-Side
- Role Verification: All operations verified via Edge Function
- JWT Validation: Supabase validates JWT tokens
- Discord API: Roles fetched directly from Discord (cannot be spoofed)
- Row-Level Security: Supabase RLS policies enforce permissions
Session Management
Login
// User logs in successfully
localStorage.setItem('sb-auth-token', token)
localStorage.setItem('sb-user-data', JSON.stringify(userData))
Logout
// Clear session
localStorage.removeItem('sb-auth-token')
localStorage.removeItem('sb-user-data')
supabase.auth.signOut()
Auto-Expiry
- Tokens expire after 1 hour (Supabase default)
- Refresh token used to renew session automatically
- Invalid tokens cleared automatically
Header Integration
The header (header.html + js/header.js) dynamically updates based on auth status:
Not Logged In
Logged In
Troubleshooting
"Not authorized" error
Cause: User doesn't have required Discord role
Solution:
- Verify Discord server membership
- Check role assignment in Discord
- Contact admin to assign correct role
Token expired
Cause: Session exceeded timeout period
Solution: Log in again (tokens auto-refresh if refresh token is valid)
Can't access internal pages
Checklist:
- Logged in via Discord
- Token exists in localStorage
- Required Discord role assigned
- Edge Function is running
- Not using private/incognito mode (clears localStorage)
Role verification failing
Debug:
// Check stored token
console.log(localStorage.getItem('sb-auth-token'))
// Test Edge Function manually
fetch('https://[project-id].supabase.co/functions/v1/discord-role-sync', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(console.log)
Setup for Developers
Prerequisites
- Supabase project created
- Discord OAuth app configured
- Edge Function deployed
Configuration Steps
- Configure Discord OAuth in Supabase:
- Dashboard → Authentication → Providers → Discord
- Add Discord Client ID and Secret
-
Set redirect URL:
https://[project-id].supabase.co/auth/v1/callback -
Deploy Edge Function:
- Set Environment Variables:
supabase secrets set DISCORD_BOT_TOKEN=your_bot_token
supabase secrets set DISCORD_GUILD_ID=your_server_id
- Update Client Configuration:
// js/login.js
const SUPABASE_URL = 'https://[project-id].supabase.co'
const SUPABASE_ANON_KEY = 'your_anon_key'