Firebase and Payload CMS: Early Look at a Client-Side Auth Strategy

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 …


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 a firebaseUID field, and add the access rules, you have to be logged in to do anything, we have added the firebaseStrategy 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 not admins.

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 a POST 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 (the true 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 and email fields. Important: The dummy-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.
  • 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.
  • 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 the authenticate 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

GitHub logo 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


Print Share Comment Cite Upload Translate Updates
APA

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/

MLA
" » Firebase and Payload CMS: Early Look at a Client-Side Auth Strategy." Aaron K Saunders | Sciencx - Saturday February 22, 2025, https://www.scien.cx/2025/02/22/firebase-and-payload-cms-early-look-at-a-client-side-auth-strategy/
HARVARD
Aaron K Saunders | Sciencx Saturday February 22, 2025 » Firebase and Payload CMS: Early Look at a Client-Side Auth Strategy., viewed ,<https://www.scien.cx/2025/02/22/firebase-and-payload-cms-early-look-at-a-client-side-auth-strategy/>
VANCOUVER
Aaron K Saunders | Sciencx - » Firebase and Payload CMS: Early Look at a Client-Side Auth Strategy. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/02/22/firebase-and-payload-cms-early-look-at-a-client-side-auth-strategy/
CHICAGO
" » Firebase and Payload CMS: Early Look at a Client-Side Auth Strategy." Aaron K Saunders | Sciencx - Accessed . https://www.scien.cx/2025/02/22/firebase-and-payload-cms-early-look-at-a-client-side-auth-strategy/
IEEE
" » Firebase and Payload CMS: Early Look at a Client-Side Auth Strategy." Aaron K Saunders | Sciencx [Online]. Available: https://www.scien.cx/2025/02/22/firebase-and-payload-cms-early-look-at-a-client-side-auth-strategy/. [Accessed: ]
rf:citation
» Firebase and Payload CMS: Early Look at a Client-Side Auth Strategy | Aaron K Saunders | Sciencx | 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.

You must be logged in to translate posts. Please log in or register.