Firebase API Key Authentication - Complete Example
December 5, 2025 · View on GitHub
Real-world implementation of custom API keys for MCP servers and programmatic access.
Overview
This example demonstrates implementing custom API keys for Firebase projects, based on the oneonone project pattern. Use this approach for:
- MCP server endpoints requiring authentication
- Programmatic API access (scripts, tools, automation)
- Server-to-server communication
- Projects where users don't need login UI
Complete Implementation
Scenario
You're building an MCP server that needs secure authentication without requiring users to log in via a web UI. Users will authenticate using API keys in the format ooo_abc123....
Step 1: Define API Key Storage Pattern
Firestore structure:
/users/{userId}/apiKeys/{keyId}
- keyId: "ooo_abc123..." (the actual key - also the document ID)
- userId: string (owner's user ID)
- active: boolean (whether key is valid)
- createdAt: timestamp
- lastUsed: timestamp (optional, updated on use)
- name: string (optional, user-friendly name like "My laptop")
Why subcollection?
- Natural organization: keys belong to users
- Enables collectionGroup queries across all users
- Easy to list keys per user
- Clear ownership model
Why keyId as document ID?
- O(1) lookup performance
- No duplicate keys possible
- Simple query pattern
Step 2: Choose Project Prefix
Pick 3-4 character abbreviation for your project:
ooo_for OneOnOnememe_for Meme Rodeobot_for Bot Socialproj_for your project
Generate key pattern:
// functions/src/utils/generateApiKey.ts
// ABOUTME: API key generation utility with project prefix and cryptographic randomness
// ABOUTME: Generates keys in format {prefix}_{randomString} for project identification
import { randomBytes } from 'crypto';
export function generateApiKey(projectPrefix: string = 'ooo'): string {
const randomPart = randomBytes(32).toString('base64url'); // 43 characters
return `${projectPrefix}_${randomPart}`;
}
// Example output: "ooo_xJ8hK9sLmN2pQ3rT4uV5wX6yZ7aB8cD9eF0gH1iJ2kL3"
Step 3: Implement Middleware Guard
functions/src/middleware/apiKeyGuard.ts:
// ABOUTME: Express middleware for API key authentication using Firestore collection group
// ABOUTME: Validates x-api-key header, checks active status, attaches userId to request
import * as admin from 'firebase-admin';
import { Request, Response, NextFunction } from 'express';
// Extend Express Request type to include userId
declare global {
namespace Express {
interface Request {
userId?: string;
}
}
}
export async function apiKeyGuard(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
const apiKey = req.headers['x-api-key'] as string;
// Check header exists and has correct prefix
if (!apiKey || !apiKey.startsWith('ooo_')) {
res.status(401).json({
success: false,
error: 'Invalid or missing API key',
hint: 'Include x-api-key header with format: ooo_xxxxx'
});
return;
}
// Query collectionGroup to find key across all users
const db = admin.firestore();
const apiKeysQuery = await db
.collectionGroup('apiKeys')
.where('keyId', '==', apiKey)
.where('active', '==', true)
.limit(1)
.get();
// Key not found or inactive
if (apiKeysQuery.empty) {
res.status(401).json({
success: false,
error: 'Invalid API key',
hint: 'Key may be inactive or deleted'
});
return;
}
// Extract userId and attach to request
const apiKeyDoc = apiKeysQuery.docs[0];
req.userId = apiKeyDoc.data().userId;
// Optional: Update lastUsed timestamp (async, don't await)
apiKeyDoc.ref.update({ lastUsed: admin.firestore.FieldValue.serverTimestamp() })
.catch(err => console.error('Failed to update lastUsed:', err));
next();
}
Key design decisions:
- Uses
collectionGroup('apiKeys')to search across all users - Checks both keyId match AND active status in single query
- Returns helpful error hints for debugging
- Attaches
userIdto request for downstream handlers - Updates
lastUsedasynchronously (doesn't block request)
Step 4: Configure Firestore Rules
firestore.rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper: Check if user is authenticated
function isAuthenticated() {
return request.auth != null;
}
// Helper: Check if user owns the document
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
// Users collection
match /users/{userId} {
// Users can read their own data
allow read: if isOwner(userId);
// Only Cloud Functions can write (server-write-only)
allow write: if false;
// API keys subcollection
match /apiKeys/{keyId} {
// Users can read their own API keys
allow read: if isOwner(userId);
// Only Cloud Functions can create/update/delete keys
allow write: if false;
}
}
// Collection group query support for middleware
match /{path=**}/apiKeys/{keyId} {
// Allow collectionGroup queries (used by middleware)
// Admin SDK bypasses rules anyway, but this documents intent
allow read: if false; // Only admin SDK can query
}
// Default deny everything else
match /{document=**} {
allow read, write: if false;
}
}
}
Why server-write-only for API keys?
- Maximum security: only Cloud Functions can create/revoke keys
- Prevents users from escalating privileges
- Single source of truth (functions control key lifecycle)
- Simpler audit trail
Step 5: Create API Key Management Endpoints
functions/src/tools/createApiKey.ts:
// ABOUTME: Handler for creating new API keys with optional name
// ABOUTME: Validates user authentication and stores key in Firestore
import * as admin from 'firebase-admin';
import { generateApiKey } from '../utils/generateApiKey';
export interface CreateApiKeyParams {
name?: string; // Optional user-friendly name
}
export interface CreateApiKeyResult {
success: boolean;
keyId?: string;
error?: string;
}
export async function handleCreateApiKey(
userId: string,
params: CreateApiKeyParams
): Promise<CreateApiKeyResult> {
try {
const db = admin.firestore();
const keyId = generateApiKey('ooo');
// Store in Firestore
await db
.collection('users')
.doc(userId)
.collection('apiKeys')
.doc(keyId)
.set({
keyId,
userId,
active: true,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
name: params.name || 'Unnamed key',
});
return {
success: true,
keyId, // Return key to user ONCE (never shown again)
};
} catch (error) {
console.error('Failed to create API key:', error);
return {
success: false,
error: 'Failed to create API key',
};
}
}
functions/src/tools/listApiKeys.ts:
// ABOUTME: Handler for listing user's API keys (without exposing full key)
// ABOUTME: Returns partial keys for security, full metadata for management
import * as admin from 'firebase-admin';
export interface ApiKeyInfo {
keyId: string; // Partially obscured
name: string;
active: boolean;
createdAt: string;
lastUsed?: string;
}
export interface ListApiKeysResult {
success: boolean;
keys?: ApiKeyInfo[];
error?: string;
}
export async function handleListApiKeys(
userId: string
): Promise<ListApiKeysResult> {
try {
const db = admin.firestore();
const keysSnapshot = await db
.collection('users')
.doc(userId)
.collection('apiKeys')
.orderBy('createdAt', 'desc')
.get();
const keys: ApiKeyInfo[] = keysSnapshot.docs.map(doc => {
const data = doc.data();
return {
// Show only prefix and last 8 chars for security
keyId: obscureApiKey(data.keyId),
name: data.name,
active: data.active,
createdAt: data.createdAt?.toDate().toISOString() || '',
lastUsed: data.lastUsed?.toDate().toISOString(),
};
});
return {
success: true,
keys,
};
} catch (error) {
console.error('Failed to list API keys:', error);
return {
success: false,
error: 'Failed to list API keys',
};
}
}
function obscureApiKey(keyId: string): string {
// "ooo_xJ8hK9s...H1iJ2kL3" (show prefix + last 8)
const parts = keyId.split('_');
if (parts.length !== 2) return keyId;
const prefix = parts[0];
const secret = parts[1];
return `${prefix}_${secret.slice(0, 3)}...${secret.slice(-8)}`;
}
functions/src/tools/revokeApiKey.ts:
// ABOUTME: Handler for revoking (deactivating) API keys
// ABOUTME: Sets active flag to false instead of deleting for audit trail
import * as admin from 'firebase-admin';
export interface RevokeApiKeyParams {
keyId: string; // Full or partial key ID
}
export interface RevokeApiKeyResult {
success: boolean;
error?: string;
}
export async function handleRevokeApiKey(
userId: string,
params: RevokeApiKeyParams
): Promise<RevokeApiKeyResult> {
try {
const db = admin.firestore();
const keyRef = db
.collection('users')
.doc(userId)
.collection('apiKeys')
.doc(params.keyId);
const keyDoc = await keyRef.get();
if (!keyDoc.exists) {
return {
success: false,
error: 'API key not found',
};
}
// Deactivate instead of delete (audit trail)
await keyRef.update({
active: false,
revokedAt: admin.firestore.FieldValue.serverTimestamp(),
});
return {
success: true,
};
} catch (error) {
console.error('Failed to revoke API key:', error);
return {
success: false,
error: 'Failed to revoke API key',
};
}
}
Step 6: Wire Up Express Routes
functions/src/index.ts:
// ABOUTME: Main entry point for Firebase Functions - exports MCP endpoint with tool routing
// ABOUTME: Configures Express app with authentication, CORS, and health check
import * as admin from 'firebase-admin';
import { onRequest } from 'firebase-functions/v2/https';
import express, { Request, Response } from 'express';
import cors from 'cors';
import { apiKeyGuard } from './middleware/apiKeyGuard';
import { handleCreateApiKey } from './tools/createApiKey';
import { handleListApiKeys } from './tools/listApiKeys';
import { handleRevokeApiKey } from './tools/revokeApiKey';
admin.initializeApp();
const app = express();
app.use(cors({ origin: true }));
app.use(express.json());
// Health check (no auth required)
app.get('/health', (_req, res) => {
res.status(200).json({ status: 'ok' });
});
// MCP endpoint (auth required)
app.post('/mcp', apiKeyGuard, async (req, res) => {
const { tool, params } = req.body;
const userId = req.userId!; // Guaranteed by apiKeyGuard
let result;
switch (tool) {
case 'create_api_key':
result = await handleCreateApiKey(userId, params);
break;
case 'list_api_keys':
result = await handleListApiKeys(userId);
break;
case 'revoke_api_key':
result = await handleRevokeApiKey(userId, params);
break;
default:
res.status(400).json({ success: false, error: 'Unknown tool' });
return;
}
res.status(200).json(result);
});
export const mcpEndpoint = onRequest(
{
invoker: 'public',
cors: true,
region: 'us-central1',
},
app
);
Step 7: Configure Hosting Rewrite
firebase.json:
{
"hosting": {
"site": "project-mcp",
"public": "hosting-mcp",
"rewrites": [
{
"source": "/**",
"function": "mcpEndpoint"
}
]
},
"emulators": {
"auth": { "port": 9099 },
"functions": { "port": 5001 },
"firestore": { "port": 8080 },
"hosting": { "port": 5000 },
"ui": { "enabled": true, "port": 4000 },
"singleProjectMode": true
}
}
Step 8: Test with Emulators
Terminal 1 - Start emulators:
firebase emulators:start
Terminal 2 - Test health check:
curl http://127.0.0.1:5000/health
# Expected: {"status":"ok"}
Terminal 3 - Create test data:
# Manually create a test user with API key via Emulator UI
# Visit http://127.0.0.1:4000
# Go to Firestore → users → [userId] → apiKeys → Add Document
# Document ID: "ooo_test123"
# Fields:
# - keyId: "ooo_test123"
# - userId: "[userId]"
# - active: true
# - createdAt: (now)
Test authenticated endpoint:
curl -X POST http://127.0.0.1:5000/mcp \
-H "Content-Type: application/json" \
-H "x-api-key: ooo_test123" \
-d '{"tool": "list_api_keys", "params": {}}'
# Expected: {"success": true, "keys": [...]}
Test missing key:
curl -X POST http://127.0.0.1:5000/mcp \
-H "Content-Type: application/json" \
-d '{"tool": "list_api_keys", "params": {}}'
# Expected: 401 {"success": false, "error": "Invalid or missing API key"}
Test invalid key:
curl -X POST http://127.0.0.1:5000/mcp \
-H "Content-Type: application/json" \
-H "x-api-key: ooo_invalid" \
-d '{"tool": "list_api_keys", "params": {}}'
# Expected: 401 {"success": false, "error": "Invalid API key"}
Production Usage
MCP Server Integration
Example MCP server (Python):
import os
import requests
API_KEY = os.environ.get("ONEONONE_API_KEY") # ooo_xxxxx
ENDPOINT = "https://project-mcp.web.app/mcp"
def call_tool(tool: str, params: dict):
response = requests.post(
ENDPOINT,
json={"tool": tool, "params": params},
headers={"x-api-key": API_KEY}
)
return response.json()
# Usage
result = call_tool("create_api_key", {"name": "My laptop"})
print(f"Created key: {result['keyId']}")
User Workflow
- User creates account (via Firebase Auth or other means)
- User requests API key (via web UI or CLI)
- Function generates key using
handleCreateApiKey - Key shown ONCE to user (they must save it)
- User configures MCP server with key in environment variable
- MCP server authenticates using
x-api-keyheader - User can list/revoke keys via web UI or API
Security Best Practices
- Never log API keys - redact from logs
- Show keys only once - at creation time
- Obscure in list view - show prefix + last chars
- Deactivate, don't delete - maintain audit trail
- Rate limit endpoints - prevent brute force
- Rotate keys periodically - encourage users to refresh
- Monitor usage - track lastUsed timestamps
Complete File Structure
functions/
├── src/
│ ├── index.ts # Express app + routing
│ ├── middleware/
│ │ └── apiKeyGuard.ts # Authentication middleware
│ ├── tools/
│ │ ├── createApiKey.ts # Create key handler
│ │ ├── listApiKeys.ts # List keys handler
│ │ └── revokeApiKey.ts # Revoke key handler
│ └── utils/
│ └── generateApiKey.ts # Key generation utility
├── package.json
└── tsconfig.json
firestore.rules # Security rules
firebase.json # Hosting + emulator config
Testing Checklist
- API key generation creates unique keys
- Keys stored in correct Firestore path
- Middleware validates prefix correctly
- Middleware rejects missing keys (401)
- Middleware rejects invalid keys (401)
- Middleware accepts valid active keys
- Middleware rejects inactive keys (401)
- userId attached to request correctly
- Create endpoint returns key once
- List endpoint obscures full keys
- Revoke endpoint deactivates keys
- Firestore rules prevent client writes
- Emulator workflow works end-to-end
- Production deployment works
- Rate limiting configured (optional)
References
- Production example:
/Users/dylanr/work/2389/oneonone - Middleware:
/Users/dylanr/work/2389/oneonone/functions/src/middleware/apiKeyGuard.ts - Main skill:
firebase-development/SKILL.md→ Authentication section