This content originally appeared on DEV Community and was authored by Aaron K Saunders
This post details a proof-of-concept integration of Firebase Authentication with Payload CMS, focusing on the client-side implementation using Next.js. The goal is to allow users to authenticate via Firebase's various sign-in methods and then use the resulting Firebase ID token to securely access data and functionality within a Payload CMS instance. This is a work in progress, and I welcome feedback and suggestions for improvement.
Video
Why Firebase and Payload?
Payload CMS is a powerful and flexible headless CMS that offers a great developer experience. Firebase provides a comprehensive suite of backend services, including a robust and easy-to-use authentication system. Combining these two allows us to:
- Leverage Firebase's Authentication Features: Support multiple sign-in methods (email/password, Google, etc.) without building custom authentication logic.
- Simplify User Management: Offload user management to Firebase, reducing the complexity of our Payload backend.
- Secure Payload Data: Use Firebase ID tokens to authenticate API requests to Payload, ensuring only authorized users can access sensitive data.
Project Setup
The project consists of two main parts:
- Payload CMS Backend: A standard Payload CMS project created with
create-payload-app
. - Next.js Frontend: A simple Next.js application with a single page to handle the authentication flow.
Firebase Configuration
- Create a Firebase Project: Create a new Firebase project in the Firebase console.
- Enable Authentication: Enable the desired sign-in methods (in this example, Email/Password).
- Obtain Service Account Credentials: Navigate to Project Settings > Service Accounts and generate a new private key. This will download a JSON file containing your service account credentials. Store this file securely.
- Initialize Firebase (Client): Create a
lib/firebase.ts
file in your Next.js project to initialize the Firebase client:
// lib/firebase.ts (Client-side)
import { initializeApp } from 'firebase/app'
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
}
// Initialize Firebase
export const app = initializeApp(firebaseConfig)
Ensure your environment variables are correctly setup in a .env
- Initialize Firebase Admin (Server): Make sure you get your service aaccount file from your Firebase Project Console
// lib/firebase-admin.ts (Server-side)
import { initializeApp, getApps, cert } from 'firebase-admin/app'
import type { ServiceAccount } from 'firebase-admin'
// get file from the project console in firebase
import serviceAccount from './firebase-service-account.json'
// Initialize Firebase Admin if not already initialized
if (!getApps().length) {
initializeApp({
credential: cert(serviceAccount as ServiceAccount),
})
}
export default getApps()[0]
Payload CMS Modifications
-
Users Collection: Modify your
Users
collection in Payload to include afirebaseUID
field, and add theaccess
rules, you have to be logged in to do anything, we have added thefirebaseStrategy
but it will be covered in the next section:
// collections/Users.ts
import { APIError, type CollectionConfig, PayloadRequest } from 'payload'
import { firebaseStrategy } from '../auth/firebaseStrategy'
import { getAuth } from 'firebase-admin/auth'
export const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'email',
},
auth: {
strategies: [
{
name: 'firebase',
authenticate: firebaseStrategy.authenticate as any,
},
],
},
access: {
read: ({ req }) => {
console.log('read', req.user?.email)
return req.user ? true : false
},
create: ({ req }) => {
console.log('create', req.user?.email)
return req.user ? true : false
},
update: ({ req }) => {
console.log('update', req.user?.email)
return req.user ? true : false
},
},
fields: [
{
name: 'firebaseUID',
type: 'text',
unique: true,
// Remove required: true to allow admin creation
admin: {
description: 'Auto-generated for Firebase users. Leave blank for admin users.',
},
},
{
name: 'role',
type: 'select',
options: ['admin', 'user'],
defaultValue: 'admin',
},
],
hooks: { },
endpoints: [],
}
-
Custom Firebase Authentication Strategy: Create a
firebaseStrategy.ts
file. For authentication, we attempt to verify the firebase token, then get the user associated with the token if one exists, we return the user, if not we create one in payload and return the user object:
// src/auth/firebaseStrategy.ts
import { getAuth } from 'firebase-admin/auth'
import '@/lib/firebase-admin' // This ensures Firebase Admin is initialized
import { AuthStrategyResult, Payload, User } from 'payload'
export const firebaseStrategy = {
name: 'firebase',
/**
* Authenticate a user with Firebase
* @param payload - The Payload instance
* @param headers - The headers object
* @returns The authenticated user or null if the user is not found or the token is invalid
*/
authenticate: async ({ payload, headers }: { payload: Payload; headers: Headers }) => {
try {
// Get the token from Authorization header
const token = headers.get('authorization')?.split(' ')[1]
if (!token) return { user: null }
// Verify Firebase token
const decodedToken = await getAuth().verifyIdToken(token, true)
// Find or create user in Payload
const { docs: users } = await payload.find({
collection: 'users',
where: {
firebaseUID: {
equals: decodedToken.uid,
},
},
})
if (users?.[0]) {
console.log('User found in Payload:', users[0])
return {
user: users[0] as any,
}
}
// Create new user if not found, even though password is optional,
// we need to provide a value for it or the user will not be created
const newUser = await payload.create({
collection: 'users',
data: {
email: decodedToken.email!,
firebaseUID: decodedToken.uid,
password: 'password', // TODO: More Research on this
role: 'user',
},
})
return {
user: newUser as unknown as User,
} as AuthStrategyResult
} catch (error) {
console.error('Firebase auth error:', error)
return { user: null }
}
},
}
-
Validation Hook We need this before validation hook to ensure. that there are no users saved in the system that do not have an id for a firebase user. Since we are focusing on the FrontEnd right now our concern is only with
users
and notadmins
.
update your user collection to include this code.
hooks: {
// Validate that firebaseUID is present for non-admin users
beforeValidate: [
({ data, operation }) => {
console.log('data', data?.email, operation)
// Skip validation for admin users
if (data?.role === 'admin') {
return data
}
// Require firebaseUID for regular users
if (!data?.firebaseUID && operation === 'create') {
throw new APIError('firebaseUID is required for non-admin users')
}
return data
},
],
},
-
Add Custom Endpoint for Revoking Token
When the user logs out of the client side of the application, we also want to ensure that the firebaseToken is invalidated. We add the custom endpoint to the user collection to perform that action. So after calling
auth.signOut()
on the front-end, you need to do aPOST
to this endpoint to finish the job; Update your user collection to include the code below.
endpoints: [
/**
* Revoke a Firebase token
* @param token - The Firebase token to revoke
* @returns A message indicating the token was revoked successfully
*/
{
path: '/revoke-token',
method: 'post',
handler: async (req: PayloadRequest) => {
try {
const token = req.headers.get('authorization')?.split(' ')[1]
if (!token) {
throw new APIError('No token provided', 401)
}
// Verify and revoke the token
const decodedToken = await getAuth().verifyIdToken(token)
await getAuth().revokeRefreshTokens(decodedToken.uid)
console.log('Token revoked successfully', decodedToken)
return Response.json(
{
message: 'Token revoked successfully',
},
{
status: 200,
},
)
} catch (error) {
return Response.json(
{
error: error,
message: 'Token revoked failed',
},
{
status: 500,
},
)
}
},
},
],
Client-Side Implementation (Next.js)
- Sign-In Component: Create a component to handle the sign-in and signout process. I kept this simple with hardcoded email and password. We are monitoring the success of failure of the code by reviewing the console logs :
'use client'
import { getAuth, signInWithEmailAndPassword, signOut } from 'firebase/auth'
import { app } from '@/lib/firebase'
const auth = getAuth(app)
export default function FirebaseLogin() {
/**
* Sign in with Firebase
* 1. Sign in with Firebase
* 2. Get Firebase token
* 3. Get Payload user
* 4. Get Media, doing this to test the token
*
* @param email
* @param password
*/
async function signInWithFirebase(email: string, password: string) {
try {
// 1. Sign in with Firebase
const result = await signInWithEmailAndPassword(auth, email, password)
if (!result.user) {
throw new Error('Failed to authenticate with Firebase')
}
// 2. Get Firebase token
const firebaseToken = await result.user.getIdToken()
localStorage.setItem('firebaseToken', firebaseToken)
// 3. Get Payload user
const response = await fetch(`/api/users?where[firebaseUID][equals]=${result.user.uid}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${firebaseToken}`,
},
})
if (!response.ok) {
throw new Error('Failed to get user: ' + response.statusText)
}
const data = await response.json()
console.log('Payload user:', data)
// 4. Get Media, doing this to test the token
const mediaResponse = await fetch('/api/media', {
method: 'GET',
headers: {
Authorization: `Bearer ${firebaseToken}`,
},
})
const mediaData = await mediaResponse.json()
console.log('Media:', mediaData)
return data.user
} catch (error) {
console.error('Authentication error:', error)
throw error
}
}
/**
* Sign out with Firebase
* 1. Sign out from Firebase
* 2. Revoke token on server
* 3. Remove token from localStorage
*/
async function signOutWithFirebase() {
try {
await signOut(auth)
const token = localStorage.getItem('firebaseToken')
if (token) {
await fetch('/api/users/revoke-token', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
localStorage.removeItem('firebaseToken')
}
} catch (error) {
console.error('Sign out error:', error)
throw error
}
}
return (
<div>
<button onClick={() => signInWithFirebase('aaron@mail.com', 'password')}>Sign in</button>
<button onClick={() => signOutWithFirebase()}>Sign Out</button>
</div>
)
}
Key Code Explanations
-
firebaseAuthStrategy.ts
:- The
authenticate
function is the core of the integration. It's called by Payload on every authenticated request. - It extracts the Firebase ID token from the
Authorization
header. -
authAdmin.verifyIdToken(token, true)
: This is crucial. It verifies the token's signature and checks if the token has been revoked (thetrue
argument). - It searches for a Payload user with a matching
firebaseUID
. - If no user is found, it creates a new Payload user, populating the
firebaseUID
andemail
fields. Important: Thedummy-password
is a placeholder and should be replaced with a more secure approach (e.g., a randomly generated, non-recoverable password). Consider not storing a password at all if you're exclusively using Firebase for authentication. - Returns user if found or created, otherwise null.
- The
-
Users.ts
(Endpoints):- The
/revoke-token
endpoint provides a way to log out users by revoking their Firebase refresh token. This prevents new ID tokens from being issued.
- The
-
pages/index.tsx
:-
signInWithEmailAndPassword
: Uses the Firebase client SDK to authenticate the user with Firebase. -
getIdToken()
: Retrieves the Firebase ID token after successful authentication. - Local Storage: The token is stored in local storage for demonstration purposes only. For production, use a more secure storage mechanism, such as HTTP-only cookies.
- API Requests: The
Authorization: Bearer ${token}
header is added to API requests to Payload, allowing theauthenticate
function to verify the token. - The sign out removes the token from localstorage, calls the firebase
signOut
function and the custom endpoint for revoking the token.
-
Important Considerations and Next Steps
- Security: This implementation has several security considerations that need to be addressed for production use:
- Token Storage: Local storage is vulnerable to XSS attacks. Use HTTP-only cookies or another secure storage mechanism.
- Password Handling: The
dummy-password
is a major security risk. Implement a proper solution, potentially eliminating the password field entirely if you're solely relying on Firebase for authentication. - Error Handling: More robust error handling is needed throughout the code.
- User Synchronization: Consider implementing webhooks or other mechanisms to keep user data synchronized between Firebase and Payload (e.g., if a user changes their email address in Firebase).
- Role-Based Access Control (RBAC): Extend the integration to map Firebase custom claims to Payload roles for more granular access control.
- Refresh Tokens: Properly implement a method to use the refresh token for when the ID token expires.
- Testing: Thorough testing is crucial to ensure the integration works as expected and is secure.
This blog post provides a starting point for integrating Firebase Authentication with Payload CMS. By addressing the security considerations and expanding upon the core concepts, you can build a robust and secure authentication system for your Payload-powered applications.
Source Code
aaronksaunders
/
payload-firebase-auth-2025
This project demonstrates how to implement Firebase Authentication with Payload CMS, allowing users to authenticate using Firebase while maintaining user data in Payload.
Firebase Authentication with Payload CMS ( Work In Progress )
Video
This project demonstrates how to implement Firebase Authentication with Payload CMS, allowing users to authenticate using Firebase while maintaining user data in Payload.
Features
- Firebase Authentication integration with Payload CMS
- Custom authentication strategy for Firebase tokens
- Automatic user creation in Payload when Firebase users sign in
- Token revocation endpoint
- Role-based access control (admin/user roles)
- TypeScript support
Prerequisites
- Firebase project with Firebase Authentication enabled
This content originally appeared on DEV Community and was authored by Aaron K Saunders

Aaron K Saunders | Sciencx (2025-02-22T21:17:14+00:00) Firebase and Payload CMS: Early Look at a Client-Side Auth Strategy. Retrieved from https://www.scien.cx/2025/02/22/firebase-and-payload-cms-early-look-at-a-client-side-auth-strategy/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.