Firebase - pacificnm/wiki-ai GitHub Wiki

๐Ÿ”ฅ Firebase Configuration

This document describes the Firebase Admin SDK configuration and authentication implementation in the wiki-ai application.


๐Ÿ“ Configuration Files

/server/config/firebase.js

The main Firebase configuration file that:

  • โœ… Initializes Firebase Admin SDK
  • โœ… Provides helper functions for Auth, Firestore, and Storage
  • โœ… Handles token verification
  • โœ… Manages user operations

/server/middleware/auth.js

Authentication middleware that:

  • โœ… Verifies Firebase ID tokens
  • โœ… Extracts user information
  • โœ… Provides role-based access control
  • โœ… Supports optional authentication

๐ŸŒ Environment Variables

Add these to your .env file:

# Firebase Admin SDK Configuration
FIREBASE_PROJECT_ID=your-firebase-project-id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@your-project.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----\n"

# Optional Firebase services
FIREBASE_DATABASE_URL=https://your-project-id-default-rtdb.firebaseio.com
FIREBASE_STORAGE_BUCKET=your-project-id.appspot.com

๐Ÿ”‘ Getting Firebase Credentials

  1. Go to Firebase Console โ†’ Project Settings โ†’ Service Accounts
  2. Generate new private key โ†’ Download JSON file
  3. Extract values from JSON:
    {
      "project_id": "your-firebase-project-id",
      "client_email": "[email protected]",
      "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
    }
    

๐Ÿš€ Initialization

The Firebase Admin SDK is initialized in server/index.js:

import { initializeFirebase } from './config/firebase.js';

async function startServer() {
  try {
    // Initialize Firebase before starting routes
    await initializeFirebase();
    
    // Continue with server setup...
  } catch (error) {
    logger.error('Server startup failed', { error: error.message });
    process.exit(1);
  }
}

๐Ÿ”ง Available Functions

Core Firebase Functions

import { 
  initializeFirebase, 
  getAuth, 
  getFirestore, 
  getStorage 
} from './config/firebase.js';

// Initialize (called once at startup)
await initializeFirebase();

// Get service instances
const auth = getAuth();
const db = getFirestore();
const storage = getStorage();

Authentication Functions

import { 
  verifyIdToken, 
  getUserById, 
  setCustomClaims 
} from './config/firebase.js';

// Verify a client's ID token
const decodedToken = await verifyIdToken(clientToken);

// Get user data by UID
const user = await getUserById('user-uid-here');

// Set custom claims (for roles)
await setCustomClaims('user-uid', { role: 'admin' });

๐Ÿ›ก๏ธ Authentication Middleware

Required Authentication

import { authenticateToken, requireUser } from '../middleware/auth.js';

// Protect routes that require authentication
router.get('/profile', 
  authenticateToken,
  requireUser, 
  (req, res) => {
    res.json({ 
      message: `Hello ${req.user.email}`,
      uid: req.user.uid,
      role: req.user.role 
    });
  }
);

Admin-Only Routes

import { authenticateToken, requireAdmin } from '../middleware/auth.js';

// Admin-only routes
router.delete('/admin/users/:id',
  authenticateToken,
  requireAdmin,
  adminController.deleteUser
);

Optional Authentication

import { optionalAuth } from '../middleware/auth.js';

// Works with or without authentication
router.get('/public-content', 
  optionalAuth, 
  (req, res) => {
    if (req.user) {
      res.json({ message: `Hello ${req.user.email}`, isAuthenticated: true });
    } else {
      res.json({ message: 'Hello anonymous user', isAuthenticated: false });
    }
  }
);

๐Ÿ‘ค User Object Structure

When authenticated, req.user contains:

{
  uid: "firebase-user-uid",
  email: "[email protected]",
  emailVerified: true,
  displayName: "John Doe",
  photoURL: "https://...",
  role: "admin", // From custom claims
  customClaims: { /* Full decoded token */ },
  firebaseUser: { /* Firebase UserRecord */ }
}

๐Ÿ” Role-Based Access Control

Setting User Roles

import { setCustomClaims } from './config/firebase.js';

// Make user an admin
await setCustomClaims('user-uid', { role: 'admin' });

// Make user a regular user
await setCustomClaims('user-uid', { role: 'user' });

// Add multiple claims
await setCustomClaims('user-uid', { 
  role: 'admin', 
  permissions: ['read', 'write', 'delete'] 
});

Using Roles in Routes

// Admin-only endpoint
router.post('/admin/categories',
  authenticateToken,
  requireAdmin,
  categoryController.createCategory
);

// User endpoint (any authenticated user)
router.get('/my-documents',
  authenticateToken,
  requireUser,
  documentController.getUserDocuments
);

// Role-specific logic
router.get('/documents/:id',
  authenticateToken,
  requireUser,
  (req, res, next) => {
    if (req.user.role === 'admin') {
      // Admins can see any document
      next();
    } else {
      // Users can only see their own documents
      // Add ownership check here
      next();
    }
  },
  documentController.getDocument
);

๐Ÿงช Testing Authentication

Valid Request Example

curl -H "Authorization: Bearer YOUR_FIREBASE_ID_TOKEN" \
     http://localhost:5000/api/profile

Expected Response

{
  "message": "Hello [email protected]",
  "uid": "firebase-user-uid",
  "role": "user"
}

Error Responses

// No token
{
  "error": "Authorization header required",
  "message": "Please provide Authorization header with Bearer token"
}

// Invalid token
{
  "error": "Authentication failed",
  "message": "Invalid or expired token"
}

// Insufficient permissions
{
  "error": "Admin access required",
  "message": "You do not have administrator privileges"
}

๐Ÿšจ Error Handling

Firebase operations include comprehensive error handling:

// Initialization errors
try {
  await initializeFirebase();
} catch (error) {
  logger.error('Firebase initialization failed', { error: error.message });
  process.exit(1);
}

// Token verification errors
try {
  const decodedToken = await verifyIdToken(token);
} catch (error) {
  // Automatically logged and handled
  throw new Error('Invalid or expired token');
}

๐Ÿ”ง Advanced Usage

Custom Token Validation

import { verifyIdToken } from './config/firebase.js';

// Custom middleware for API keys + Firebase
export async function hybridAuth(req, res, next) {
  const apiKey = req.headers['x-api-key'];
  const authHeader = req.headers.authorization;
  
  if (apiKey && isValidApiKey(apiKey)) {
    req.user = { type: 'api', key: apiKey };
    return next();
  }
  
  if (authHeader && authHeader.startsWith('Bearer ')) {
    try {
      const token = authHeader.substring(7);
      const decoded = await verifyIdToken(token);
      req.user = { type: 'firebase', ...decoded };
      return next();
    } catch (error) {
      return res.status(401).json({ error: 'Invalid token' });
    }
  }
  
  return res.status(401).json({ error: 'Authentication required' });
}

Firestore Integration

import { getFirestore } from './config/firebase.js';

export async function syncUserToFirestore(userRecord) {
  const db = getFirestore();
  
  await db.collection('users').doc(userRecord.uid).set({
    email: userRecord.email,
    displayName: userRecord.displayName,
    emailVerified: userRecord.emailVerified,
    createdAt: new Date(),
    lastLoginAt: new Date()
  }, { merge: true });
}

๐Ÿ“‹ Security Best Practices

  1. Environment Variables: Never commit Firebase private keys to version control
  2. Token Validation: Always verify tokens on the server side
  3. Custom Claims: Use for role-based access control
  4. HTTPS Only: Use HTTPS in production for token security
  5. Token Expiry: Firebase ID tokens expire after 1 hour
  6. Error Handling: Don't expose sensitive error details to clients

๐Ÿ”— Resources