[Next.js14] Firebase Authentication with Google sign-in using cookies, middleware, and Server Actions

This post provides a explanation of implementing user authentication in a Next.js 14 application using Firebase Authentication with Google sign-in. It utilizes cookies and server actions to effectively manage user sessions. Since this is a demo, there are no token updates or rigorous checking of tokens. Please customize it accordingly according to your project specifications.

Repository:
https://github.com/yutakusuno/nextjs-firebase-auth-google-signin

Overview

UI

As for the components I implemented, all I have is the home page and a very simple header. I have kept it simple so that you can easily utilize it in your projects.

Before signing in (localhost:3000/)

Image description

After signing in (localhost:3000/home)

Image description

Project Structure

├── actions // added
│ └── auth-actions.ts
├── app
│ ├── home // added
│ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx // fixed
│ └── page.tsx
├── components // added
│ └── header.tsx
├── hooks // added
│ └── use-user-session.ts
├── libs // added
│ └── firebase
│ ├── auth.ts
│ └── config.ts
├── public
├── .env // added
├── .eslintrc.json
├── .gitignore
├── .prettierrc.yaml
├── constants.ts // added
├── middleware.ts // added
├── next-env.d.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── README.md
├── tailwind.config.ts
└── tsconfig.json
Enter fullscreen mode
Exit fullscreen mode

Package Versions (Package.json)

{ ... "dependencies": { "firebase": "^10.11.0", "next": "14.2.1", "react": "^18", "react-dom": "^18" }, "devDependencies": { "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", "eslint-config-next": "14.2.1", "eslint-config-prettier": "^9.1.0", "postcss": "^8", "tailwindcss": "^3.4.1", "typescript": "^5" }
}
Enter fullscreen mode
Exit fullscreen mode

Code

Now let’s look at the implementation.

Environment Variables

This environment file .env securely stores Firebase project configuration values (API key, auth domain, etc.).

If you have not yet set up your Firebase project, click here.

# .env NEXT_PUBLIC_FIREBASE_API_KEY="your api key"
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="your auth domain"
NEXT_PUBLIC_FIREBASE_PROJECT_ID="your project id"
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="you storage bucket"
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="your messaging sender id"
NEXT_PUBLIC_FIREBASE_APP_ID="your app id"
Enter fullscreen mode
Exit fullscreen mode

Firebase Configuration

The libs/firebase/config.ts file handles the initialization and configuration of the Firebase app.

// libs/firebase/config.ts import { getAuth } from 'firebase/auth';
import { initializeApp, getApps } from 'firebase/app'; // Load .env variables
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,
}; const firebaseApp = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; export const firebaseAuth = getAuth(firebaseApp);
Enter fullscreen mode
Exit fullscreen mode
  • Initializes the Firebase app using initializeApp or retrieves an existing instance.
  • Exports the firebaseAuth instance for accessing Firebase authentication services.

Firebase Authentication

The libs/firebase/auth.ts file encapsulates Firebase authentication logic.

// libs/firebase/auth.ts import { type User, GoogleAuthProvider, signInWithPopup, onAuthStateChanged as _onAuthStateChanged,
} from 'firebase/auth'; import { firebaseAuth } from './config'; export function onAuthStateChanged(callback: (authUser: User | null) => void) { return _onAuthStateChanged(firebaseAuth, callback);
} export async function signInWithGoogle() { const provider = new GoogleAuthProvider(); try { await signInWithPopup(firebaseAuth, provider); } catch (error) { console.error('Error signing in with Google', error); }
} export async function signOutWithGoogle() { try { await firebaseAuth.signOut(); } catch (error) { console.error('Error signing out with Google', error); }
}
Enter fullscreen mode
Exit fullscreen mode
  • onAuthStateChanged: Listens for changes in the user’s authentication state.
  • signInWithGoogle: Initiates Google sign-in using a popup window.
  • signOutWithGoogle: Signs out the current user from Firebase.

Constants

// constants.ts export const ROOT_ROUTE = '/';
export const HOME_ROUTE = '/home'; export const SESSION_COOKIE_NAME = 'user_session'; 
Enter fullscreen mode
Exit fullscreen mode

Session Management in Server Actions

In actions/auth-actions.ts, it implements server actions for creating and removing session cookies.

// actions/auth-actions.ts 'use server'; import { cookies } from 'next/headers';
import { redirect } from 'next/navigation'; import { HOME_ROUTE, ROOT_ROUTE, SESSION_COOKIE_NAME } from '@/constants'; export async function createSession(uid: string) { cookies().set(SESSION_COOKIE_NAME, uid, { httpOnly: true, secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 24, // One day path: '/', }); redirect(HOME_ROUTE);
} export async function removeSession() { cookies().delete(SESSION_COOKIE_NAME); redirect(ROOT_ROUTE);
}
Enter fullscreen mode
Exit fullscreen mode
  • createSession: Sets an HTTP-only cookie named SESSION_COOKIE_NAME containing the user ID and redirects to the home page.
  • removeSession: Deletes the SESSION_COOKIE_NAME cookie and redirects to the root page.

Session Management in Client

In hooks/use-user-session.ts, it provides a custom hook for managing user sessions.

// hooks/use-user-session.ts import { useEffect, useState } from 'react'; import { onAuthStateChanged } from '../libs/firebase/auth';
import { createSession, removeSession } from '@/actions/auth-actions'; export function useUserSession(InitSession: string | null) { const [userUid, setUserUid] = useState<string | null>(InitSession); // Listen for changes to the user session useEffect(() => { const unsubscribe = onAuthStateChanged(async (authUser) => { if (authUser) { setUserUid(authUser.uid); await createSession(authUser.uid); } else { setUserUid(null); await removeSession(); } }); return () => unsubscribe(); }, []); return userUid;
}
Enter fullscreen mode
Exit fullscreen mode
  • useUserSession:

    • Accepts an initial session ID.
    • Maintains the current user ID (userUid) in state.
    • Listens for Firebase auth state changes using onAuthStateChanged.
    • Updates userUid and calls createSession or removeSession based on the user’s signed-in state.
    • Returns the current userUid.

Application Layout

//app/layout.tsx import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { cookies } from 'next/headers'; import Header from '@/components/header'; // added
import { SESSION_COOKIE_NAME } from '@/constants'; // added
import './globals.css'; const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app',
}; export default async function RootLayout({ children,
}: Readonly<{ children: React.ReactNode;
}>) { // added const session = cookies().get(SESSION_COOKIE_NAME)?.value || null; return ( <html lang='en'> <body className={inter.className}> <Header session={session} /> // added {children} </body>
 </html>
 );
} 
Enter fullscreen mode
Exit fullscreen mode
  • Retrieves the session cookie from the request headers.
  • Renders the Header component, passing the session as a prop.
  • Displays the application’s content.

By passing session information from the RSC to the RCC, the Header component for authenticated users can be loaded from the beginning if the user is already logged in.

Header Component

// components/header.tsx 'use client'; import { useUserSession } from '@/hooks/use-user-session';
import { signInWithGoogle, signOutWithGoogle } from '@/libs/firebase/auth'; export function Header({ session }: { session: string | null }) { const userSessionId = useUserSession(session); const handleSignIn = async () => { await signInWithGoogle(); }; const handleSignOut = async () => { await signOutWithGoogle(); }; if (!userSessionId) { return ( <header> <button onClick={handleSignIn}>Sign In</button>
 </header>
 ); } return ( <header> <nav> <ul> <li> <a href='#'>Menu A</a>
 </li>
 <li> <a href='#'>Menu B</a>
 </li>
 <li> <a href='#'>Menu C</a>
 </li>
 </ul>
 </nav>
 <button onClick={handleSignOut}>Sign Out</button>
 </header>
 );
} export default Header;
Enter fullscreen mode
Exit fullscreen mode
  • Uses useUserSession to get the current user’s session ID.
  • Displays a sign-in button if the user is not signed in.
  • Displays a sign-out button and navigation links if the user is signed in.

Middleware

// middleware.ts import { type NextRequest, NextResponse } from 'next/server';
import { HOME_ROUTE, ROOT_ROUTE, SESSION_COOKIE_NAME } from './constants'; const protectedRoutes = [HOME_ROUTE]; export default function middleware(request: NextRequest) { const session = request.cookies.get(SESSION_COOKIE_NAME)?.value || ''; // Redirect to login if session is not set if (!session && protectedRoutes.includes(request.nextUrl.pathname)) { const absoluteURL = new URL(ROOT_ROUTE, request.nextUrl.origin); return NextResponse.redirect(absoluteURL.toString()); } // Redirect to home if session is set and user tries to access root if (session && request.nextUrl.pathname === ROOT_ROUTE) { const absoluteURL = new URL(HOME_ROUTE, request.nextUrl.origin); return NextResponse.redirect(absoluteURL.toString()); }
}
Enter fullscreen mode
Exit fullscreen mode
  • Identifies protected routes (e.g., home page).
  • Redirects users to the login page if they are not signed in and trying to access a protected route.
  • Redirects signed-in users to the home page if they try to access the root page.

Overall Implementation

The application utilizes the following flow:

  1. The user visits the application.
  2. The middleware checks the session cookie.
    • If the user is not signed in and tries to access a protected route, they are redirected to the login page.
    • If the user is signed in and tries to access the root page, they are redirected to the home page.
  3. On the login page, the user clicks the “Sign In” button.
  4. The signInWithGoogle function is called, initiating the Google sign-in process.
  5. Upon successful sign-in, Firebase sends the user’s profile information to the application.
  6. The onAuthStateChanged function detects the user’s signed-in state.
  7. The useUserSession hook updates the userUid state and calls the createSession function.
  8. The createSession function sets the session cookie and redirects the user to the home page.
  9. The user can now navigate through the protected areas of the application.
  10. To sign out, the user clicks the “Sign Out” button in the header.
  11. The signOutWithGoogle function is called, signing the user out of Firebase.
  12. The onAuthStateChanged function detects the user’s signed-out state.
  13. The useUserSession hook updates the userUid state to null and calls the removeSession.

If you like it, feel free to adjust the code snippets provided and have them customized to your project’s specific requirements and preferences.

Repository:
https://github.com/yutakusuno/nextjs-firebase-auth-google-signin

That is about it. Happy coding!


Discover more from Coursity

Subscribe to get the latest posts sent to your email.

Leave a Comment

Your email address will not be published. Required fields are marked *

Discover more from Coursity

Subscribe now to keep reading and get access to the full archive.

Continue reading

Scroll to Top