Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/asubap/website/llms.txt

Use this file to discover all available pages before exploring further.

The BAP Beta Tau platform currently stores Supabase session tokens in localStorage, which is the default behaviour of the Supabase JS client. While simple to implement, localStorage is accessible to any JavaScript running on the page, creating an XSS token-theft risk. This document outlines the complete migration plan — sourced from COOKIE_MIGRATION.md — to move authentication to HttpOnly cookies managed by the backend.
This migration is planned but not yet implemented. The current codebase uses localStorage-backed Supabase sessions as documented in Auth Context. Use this page as the engineering specification for the migration.

Current vs. Target Architecture

Current Architecture

Browser (localStorage) ←→ Supabase Auth
  • Supabase JS client stores access_token and refresh_token in localStorage
  • Frontend reads tokens and passes them as Authorization: Bearer headers
  • Any XSS vulnerability can steal the token

Target Architecture

Browser ←→ Backend API (HttpOnly Cookies) ←→ Supabase Auth
  • Backend sets HttpOnly; Secure; SameSite=Strict cookies on login
  • Frontend never touches tokens — cookies are sent automatically
  • XSS attacks cannot read HttpOnly cookies

Security Benefits

ConcernlocalStorage (current)HttpOnly Cookies (target)
XSS token theft❌ Vulnerable✅ Protected
JavaScript access❌ Readable by any script✅ Inaccessible to JS
CSRF protection❌ NoneSameSite=Strict
Expiration control❌ ManualmaxAge in cookie
Audit trail❌ Client-only✅ Server-side logs

Backend Changes

Step 1: Install Dependencies

npm install cookie-parser jsonwebtoken @supabase/supabase-js

Step 2: Auth Middleware

File: backend/middleware/auth.js
const { createClient } = require('@supabase/supabase-js');

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SERVICE_ROLE_KEY  // Server-side key
);

async function validateSession(req, res, next) {
  const sessionToken = req.cookies.session_token;

  if (!sessionToken) {
    return res.status(401).json({ error: 'No session found' });
  }

  const { data: { user }, error } = await supabase.auth.getUser(sessionToken);

  if (error || !user) {
    res.clearCookie('session_token', {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      path: '/'
    });
    return res.status(401).json({ error: 'Invalid session' });
  }

  req.user = user;
  req.sessionToken = sessionToken;
  next();
}

module.exports = { validateSession };

Step 3: Auth Routes

File: backend/routes/auth.js The cookie configuration used for all auth cookies:
const COOKIE_OPTIONS = {
  httpOnly: true,                               // No JS access
  secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
  sameSite: 'strict',                           // CSRF protection
  maxAge: 7 * 24 * 60 * 60 * 1000,             // 7 days
  path: '/'
};
router.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;

  const { data, error } = await supabase.auth.signInWithPassword({
    email, password
  });

  if (error) return res.status(401).json({ error: error.message });

  // Set HttpOnly cookies — tokens never reach the browser JS
  res.cookie('session_token', data.session.access_token, COOKIE_OPTIONS);
  res.cookie('refresh_token', data.session.refresh_token, COOKIE_OPTIONS);

  // Return user info only — NOT tokens
  res.json({ user: { id: data.user.id, email: data.user.email } });
});

Step 4: Update CORS Configuration

CORS must allow credentials for cookies to be sent cross-origin:
// backend/server.js
app.use(cookieParser());
app.use(cors({
  origin: process.env.FRONTEND_URL || 'http://localhost:5173',
  credentials: true, // CRITICAL: allows cookies to be sent
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

Step 5: Update /users Endpoint

After migration, /users reads the user from req.user (set by middleware) instead of from the Authorization header:
router.post('/users', validateSession, async (req, res) => {
  const userEmail = req.user.email; // Injected by validateSession middleware

  const user = await db.query('SELECT * FROM members WHERE email = $1', [userEmail]);

  if (!user || user.is_archived) {
    res.clearCookie('session_token', { /* COOKIE_OPTIONS */ });
    res.clearCookie('refresh_token', { /* COOKIE_OPTIONS */ });
    return res.status(403).json({ error: 'Member is archived' });
  }

  res.json({ type: user.role, companyName: user.company_name });
});

Frontend Changes

Step 1: New Auth Service

File: src/services/authService.ts
const API_URL = import.meta.env.VITE_BACKEND_URL;

export const authService = {
  async login(email: string, password: string) {
    const response = await fetch(`${API_URL}/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include', // CRITICAL: sends cookies
      body: JSON.stringify({ email, password })
    });
    if (!response.ok) throw new Error((await response.json()).error || 'Login failed');
    return response.json();
  },

  async googleLogin(idToken: string) {
    const response = await fetch(`${API_URL}/auth/google`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ token: idToken })
    });
    if (!response.ok) throw new Error((await response.json()).error || 'Google login failed');
    return response.json();
  },

  async logout() {
    await fetch(`${API_URL}/auth/logout`, {
      method: 'POST',
      credentials: 'include'
    });
  },

  async getSession() {
    const response = await fetch(`${API_URL}/auth/session`, {
      credentials: 'include'
    });
    if (!response.ok) return null;
    return response.json();
  },

  async getUserRole(email: string) {
    const response = await fetch(`${API_URL}/users`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include', // Cookies sent automatically — no Bearer header needed
      body: JSON.stringify({ user_email: email })
    });
    if (!response.ok) throw new Error((await response.json()).error || 'Failed to fetch role');
    return response.json();
  }
};

Step 2: Updated Auth Provider

The new AuthProvider drops the Supabase client entirely for auth operations and uses authService instead:
// Key changes from current authProvider.tsx:

// REMOVE: import { supabase } from "./supabaseClient.ts";
// ADD:    import { authService } from "../../services/authService";

// REMOVE: Session type from @supabase/supabase-js
// ADD:    Custom User type: { id: string; email: string }

// Context value changes:
interface AuthContextType {
  user: User | null;          // replaces: session: Session | null
  role: RoleType;
  loading: boolean;
  authError: string | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  googleLogin: (idToken: string) => Promise<void>;
  logout: () => Promise<void>;
  // REMOVED: setSession, setRole, setAuthError (no longer needed externally)
}

Step 3: Update All Fetch Calls

Every authenticated API call must add credentials: 'include' — this tells the browser to send the HttpOnly cookie with the request:
// Before (current pattern):
const response = await fetch(`${VITE_BACKEND_URL}/events`, {
  headers: { Authorization: `Bearer ${session.access_token}` }
});

// After (cookie pattern):
const response = await fetch(`${VITE_BACKEND_URL}/events`, {
  credentials: 'include' // Cookie is sent automatically
  // No Authorization header needed
});
Find all fetch calls that need updating:
grep -r "fetch(" Frontend/src/ | grep -v "node_modules"

Migration Phases

1

Phase 1: Backend Setup

  • Install cookie-parser dependency
  • Create middleware/auth.js with validateSession
  • Create routes/auth.js with login / logout / session / refresh / Google endpoints
  • Update routes/users.js to use cookie middleware
  • Enable CORS with credentials: true
  • Add SUPABASE_SERVICE_ROLE_KEY to backend environment variables
  • Test with curl before touching the frontend
2

Phase 2: Frontend Migration

  • Create src/services/authService.ts
  • Rewrite src/context/auth/authProvider.tsx to use authService
  • Update login page to call authService.login() / authService.googleLogin()
  • Update logout component to call authService.logout()
  • Add credentials: 'include' to all fetch() calls
  • Remove Supabase client auth imports (keep client only if used for non-auth operations)
3

Phase 3: Testing and Validation

Verify each flow end-to-end:
  • ✅ Email/password login sets cookies
  • ✅ Google OAuth login sets cookies
  • ✅ Authenticated API requests send cookies automatically
  • ✅ Archiving a member clears cookies within 30 seconds
  • ✅ Token refresh sets new cookies
  • ✅ Logout clears cookies and redirects
  • ✅ Session persists across page refreshes
  • ✅ Works in Chrome, Firefox, Safari
4

Phase 4: Security Hardening

  • Set Secure: true in production (HTTPS-only)
  • Confirm SameSite: 'strict' is set
  • Add rate limiting on auth endpoints (5 req / 15 min)
  • Install and configure helmet.js for security headers
  • Add Content Security Policy headers
  • Set up HTTPS redirect in production
  • Enable audit logging for all auth events
  • Restrict CORS origin to production domain only

Security Hardening Code

Rate Limiting

const rateLimit = require('express-rate-limit');

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5,                    // 5 requests per window per IP
  message: 'Too many login attempts, please try again later'
});

app.use('/api/auth/login', authLimiter);
app.use('/api/auth/google', authLimiter);

Helmet.js Security Headers

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  hsts: {
    maxAge: 31536000,       // 1 year
    includeSubDomains: true,
    preload: true
  }
}));

Rollback Plan

If migration causes critical issues:
1

Revert frontend

Restore src/context/auth/authProvider.tsx from git. Remove src/services/authService.ts. Remove credentials: 'include' from fetch calls.
2

Disable backend auth routes

Comment out or remove the new cookie-based auth routes. Keep existing Authorization: Bearer header validation in place.
3

Verify old flow

Confirm localStorage-backed Supabase sessions work for login, logout, and protected routes.

Key Implementation Notes

If the frontend (asubap.com) and backend (asubap-backend.vercel.app) are on different domains, SameSite: 'strict' will block cross-site cookie sending. You may need SameSite: 'none' + Secure: true for cross-domain setups, or proxy the API through the same domain.
HttpOnly cookies work in mobile web browsers and web views. Native mobile apps (iOS/Android) that bypass the browser’s cookie store will need a different token delivery mechanism.

Build docs developers (and LLMs) love