The Authentication Gap: How to Properly Authenticate Firebase Data Connect in Cloud Functions
Introduction
Firebase Data Connect's @auth(level: USER) directive requires authenticated user context, but Cloud Functions run in a server context with Admin SDK privileges. Simply verifying a token doesn't provide that context to Data Connect.
We hit this error repeatedly: "unauthenticated: this operation requires a signed-in user" - even after verifying tokens with Admin SDK. This article explains the authentication gap and how we solved it by creating authenticated DataConnect instances that properly bridge Firebase Auth tokens to Data Connect operations.
The Problem
Here's what we tried first (and why it failed):
// ❌ WRONG: This doesn't work
export const getUserData = onRequest(async (req, res) => {
const token = req.headers.authorization?.split('Bearer ')[1]
const decodedToken = await admin.auth().verifyIdToken(token)
// This still fails with "unauthenticated"
const result = await getUserById(dataConnect, { userId: decodedToken.uid })
res.json(result.data)
})
Why it fails: Verifying the token with Admin SDK doesn't create authenticated user context for Data Connect. Data Connect needs an authenticated Firebase app instance, not just token verification.
Understanding Data Connect Auth Levels
Firebase Data Connect supports two authentication levels:
@auth(level: PUBLIC)
No authentication required. Used for:
- Public data
- System operations (sync, setup)
- Demo/development operations
query GetPublicAssets @auth(level: PUBLIC) {
creativeAssets(where: { status: { eq: "published" } }) {
id
title
}
}
@auth(level: USER)
Requires authenticated user context. Used for:
- User-specific data
- Protected resources
- Operations requiring user identity
query GetUserCollections @auth(level: USER) {
collections(where: { userId: { eq: $userId } }) {
id
name
}
}
The gap: Data Connect validates user context at the GraphQL layer, not just token verification. You need to create a DataConnect instance with authenticated user context.
The Solution: Authenticated DataConnect Instance
Here's how we create authenticated DataConnect instances:
import { initializeApp } from 'firebase/app'
import { getDataConnect } from 'firebase/data-connect'
import { connectorConfig } from '@creativeos/platform'
export async function createAuthenticatedDataConnect(userToken: string) {
// CRITICAL: Use complete Firebase config, not demo values
const app = initializeApp({
projectId: process.env.FIREBASE_PROJECT_ID,
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
// ... other required config
}, 'authenticated-dataconnect')
// Create DataConnect instance
const dataConnect = getDataConnect(app, connectorConfig)
// CRITICAL: Set authentication context
// This bridges the user token to Data Connect
await dataConnect.setAuthToken(userToken)
// Verify token to get user info
const decodedToken = await admin.auth().verifyIdToken(userToken)
return { dataConnect, decodedToken }
}
Key points:
- Complete Firebase config: Not demo values - real project configuration
- Set auth token:
setAuthToken()provides user context to Data Connect - Verify token: Still verify with Admin SDK for user info
HTTP Functions Pattern
For HTTP functions, we extract the token and create an authenticated instance:
import { Request, Response } from 'express'
import * as admin from 'firebase-admin'
export function extractBearerToken(req: Request, res: Response): string | null {
const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({
error: 'Authentication required',
message: 'Please provide a valid Bearer token in the Authorization header'
})
return null
}
return authHeader.split('Bearer ')[1]
}
export async function verifyFirebaseToken(token: string, res: Response) {
try {
const decodedToken = await admin.auth().verifyIdToken(token)
return decodedToken
} catch (error) {
console.error('❌ Authentication verification failed:', error)
res.status(401).json({
error: 'Authentication verification failed',
message: error instanceof Error ? error.message : 'Invalid authentication token'
})
return null
}
}
export async function createAuthenticatedDataConnectFromRequest(
req: Request,
res: Response
): Promise<{ dataConnect: any; decodedToken: admin.auth.DecodedIdToken } | null> {
const userToken = extractBearerToken(req, res)
if (!userToken) return null
try {
const { createAuthenticatedDataConnect } = await import('../auth-dataconnect')
const authResult = await createAuthenticatedDataConnect(userToken)
return {
dataConnect: authResult.dataConnect,
decodedToken: authResult.decodedToken
}
} catch (error) {
console.error('❌ Failed to create authenticated DataConnect:', error)
res.status(401).json({
error: 'Authentication failed',
message: error instanceof Error ? error.message : 'Invalid token'
})
return null
}
}
Usage in HTTP function:
export const getUserCollections = onRequest(async (req, res) => {
// Create authenticated DataConnect instance
const auth = await createAuthenticatedDataConnectFromRequest(req, res)
if (!auth) return // Error already sent
const { dataConnect, decodedToken } = auth
// Now user-level queries work!
const result = await getUserCollections(dataConnect, {
userId: decodedToken.uid
})
res.json(result.data)
})
Callable Functions Pattern
Callable functions include user auth automatically:
export const createCollection = onCall(async (request) => {
// request.auth is automatically populated by Firebase
if (!request.auth) {
throw new HttpsError('unauthenticated', 'User must be authenticated')
}
const userId = request.auth.uid // Backend-derived, always safe
// Create authenticated DataConnect from user token
// (You'd need to get token from request, implementation varies)
const dataConnect = await createAuthenticatedDataConnect(userToken)
// Execute user-level mutation
const result = await createCollection(dataConnect, {
userId,
name: request.data.name
})
return result.data
})
Security note: Always use request.auth.uid for user identity, never trust client-provided UIDs.
Security Best Practices
1. Always Verify Tokens
// ✅ CORRECT: Verify token
const decodedToken = await admin.auth().verifyIdToken(token)
// ❌ WRONG: Trust client-provided UID
const userId = req.body.userId // Never do this!
2. Use Complete Firebase Configuration
// ✅ CORRECT: Real project config
const app = initializeApp({
projectId: process.env.FIREBASE_PROJECT_ID,
apiKey: process.env.FIREBASE_API_KEY,
// ... all required fields
})
// ❌ WRONG: Demo values
const app = initializeApp({
projectId: 'demo-project',
apiKey: 'demo-key' // Won't work with real authentication
})
3. Whitelist Claims Explicitly
// ✅ CORRECT: Explicit claims
const customClaims = {
role: request.data.role, // Only allow specific fields
teamId: request.data.teamId
}
// ❌ WRONG: Spread operator (security risk!)
const customClaims = {
...request.data // Allows arbitrary claim injection!
}
4. Backend-Derived Identity Only
// ✅ CORRECT: Use verified token
const userId = decodedToken.uid
// ❌ WRONG: Trust client
const userId = req.body.userId
Common Pitfalls
Pitfall 1: Incomplete Firebase Config
Error: "unauthenticated: this operation requires a signed-in user"
Cause: Using demo Firebase config values
Fix: Use complete project configuration from environment variables
Pitfall 2: Token Verification Without Context
Error: Token verifies successfully but Data Connect still fails
Cause: Verifying token doesn't create Data Connect user context
Fix: Call setAuthToken() on DataConnect instance
Pitfall 3: Using Admin SDK to Bypass Auth
Error: Trying to use Admin SDK privileges to bypass @auth(level: USER)
Cause: Data Connect doesn't have admin bypass like Firestore
Fix: Create authenticated DataConnect instances with user tokens
Pitfall 4: Spread Operator in Callable Functions
Error: Arbitrary claim injection vulnerability
Cause: Using ...request.data allows attackers to set any custom claim
Fix: Whitelist claims explicitly
Helper Functions Pattern
To reduce boilerplate, we created helper functions:
// Extract and verify in one step
export async function getAuthenticatedToken(
req: Request,
res: Response
): Promise<{ token: string; decoded: admin.auth.DecodedIdToken } | null> {
const token = extractBearerToken(req, res)
if (!token) return null
const decoded = await verifyFirebaseToken(token, res)
if (!decoded) return null
return { token, decoded }
}
// Complete auth flow for Data Connect
export async function createAuthenticatedDataConnectFromRequest(
req: Request,
res: Response
): Promise<{ dataConnect: any; decodedToken: admin.auth.DecodedIdToken } | null> {
const userToken = extractBearerToken(req, res)
if (!userToken) return null
try {
const { createAuthenticatedDataConnect } = await import('../auth-dataconnect')
const authResult = await createAuthenticatedDataConnect(userToken)
return {
dataConnect: authResult.dataConnect,
decodedToken: authResult.decodedToken
}
} catch (error) {
console.error('❌ Failed to create authenticated DataConnect:', error)
sendError(res, 401, 'Authentication failed', error instanceof Error ? error.message : 'Invalid token')
return null
}
}
Usage becomes simple:
export const getUserData = onRequest(async (req, res) => {
const auth = await createAuthenticatedDataConnectFromRequest(req, res)
if (!auth) return
const { dataConnect, decodedToken } = auth
// Use authenticated DataConnect...
})
Environment-Specific Configuration
Development (Emulator)
if (process.env.FUNCTIONS_EMULATOR === 'true') {
// Connect to Data Connect emulator
connectDataConnectEmulator(dataConnect, 'localhost', 9399)
}
Production
// Uses service account credentials for authentication
// DataConnect instance inherits auth from Firebase app config
const dataConnect = getDataConnect(app, connectorConfig)
Real-World Example
Here's a complete example from our codebase:
export const getUserCollections = onRequest(async (req, res) => {
setCorsHeaders(res)
if (handleOptions(req, res)) return
if (!validateMethod(req, res, ['GET'])) return
// Create authenticated DataConnect instance
const auth = await createAuthenticatedDataConnectFromRequest(req, res)
if (!auth) return // Error already sent
const { dataConnect, decodedToken } = auth
try {
// Execute user-level query
const result = await getUserCollections(dataConnect, {
userId: decodedToken.uid
})
res.json({
success: true,
data: result.data
})
} catch (error) {
console.error('Failed to get user collections:', error)
sendError(res, 500, 'Failed to fetch collections', error instanceof Error ? error.message : 'Unknown error')
}
})
Troubleshooting
Issue: "unauthenticated: this operation requires a signed-in user"
Check:
- ✅ Token is valid and not expired
- ✅ Using complete Firebase config (not demo values)
- ✅ Called
setAuthToken()on DataConnect instance - ✅ Query has
@auth(level: USER)directive
Issue: Token verifies but Data Connect fails
Cause: Token verification ≠ Data Connect authentication context
Fix: Create authenticated DataConnect instance with setAuthToken()
Issue: Works in emulator but fails in production
Check:
- ✅ Environment variables set correctly
- ✅ Service account has proper permissions
- ✅ Firebase project configuration matches
Best Practices
- Always verify tokens: Never trust client-provided authentication
- Use helper functions: Reduce boilerplate and errors
- Complete Firebase config: Use real project values, not demos
- Backend-derived identity: Always use
decodedToken.uid - Explicit claims: Whitelist claims, never use spread operator
- Error handling: Provide clear error messages for debugging
Conclusion
Firebase Data Connect authentication requires bridging the gap between token verification and authenticated user context. By creating authenticated DataConnect instances with setAuthToken(), we ensure user-level queries work correctly in Cloud Functions.
The key insight: Token verification ≠ Data Connect authentication context. You need both - verify the token for security, and set it on DataConnect for user context.
The patterns we've shared here are production-tested and handle thousands of authenticated requests daily. Whether you're building user-specific queries, protected mutations, or any feature requiring user context, proper authentication ensures your Data Connect operations work reliably.