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/)
When a user clicks the sign in button
After signing in (localhost:3000/home)
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
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"
}
}
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"
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);
- 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 {
const result = await signInWithPopup(firebaseAuth, provider);
if (!result || !result.user) {
throw new Error('Google sign in failed');
}
return result.user.uid;
} 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);
}
}
-
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';
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);
}
-
createSession
: Sets a cookie namedSESSION_COOKIE_NAME
containing the user ID and redirects to the home page.- HTTP-only: The cookie is flagged as HTTP-only, enhancing security by preventing client-side JavaScript from accessing its content.
-
Secure: In a production environment, the
secure
flag is set, ensuring the cookie is transmitted only over HTTPS connections. - Max Age: The cookie expires after one day (24 hours) of inactivity.
-
removeSession
: Deletes theSESSION_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';
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);
} else {
setUserUid(null);
}
});
return () => unsubscribe();
}, []);
return userUid;
}
-
useUserSession
:- Accepts an initial session ID.
- Maintains the current user ID (
userUid
) in state. - Listens for Firebase auth state changes using
onAuthStateChanged
. - 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>
);
}
- 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';
import { createSession, removeSession } from '@/actions/auth-actions';
export function Header({ session }: { session: string | null }) {
const userSessionId = useUserSession(session);
const handleSignIn = async () => {
const userUid = await signInWithGoogle();
if (userUid) {
await createSession(userUid);
}
};
const handleSignOut = async () => {
await signOutWithGoogle();
await removeSession();
};
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;
- 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.
- Based on the event handlers (
handleSignIn
orhandleSignOut
), signing in or signing out process. At the same time, calling server actionscreateSession
orremoveSession
.
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());
}
}
- 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:
- The user visits the application.
- 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.
- On the login page, the user clicks the "Sign In" button.
- The
signInWithGoogle
function is called, initiating the Google sign-in process. - Upon successful sign-in, Firebase sends the user's profile information to the application.
- The
onAuthStateChanged
function detects the user's signed-in state. - The
useUserSession
hook updates theuserUid
state. - The createSession function sets the session cookie and redirects the user to the home page.
- The user can now navigate through the protected areas of the application.
- To sign out, the user clicks the "Sign Out" button in the header.
- The
signOutWithGoogle
function is called, signing the user out of Firebase. - The
onAuthStateChanged
function detects the user's signed-out state. - The
useUserSession
hook updates theuserUid
state tonull
and calls theremoveSession
.
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!
Top comments (6)
Will this work for SSR page? I’m seeing you’re managing token using cookie, but cookie isn’t available in server side
Hi @tungdnt !
Unfortunately, this will not work. The signInWithPopup which authenticates a Firebase client using a popup can work in client components. As far as I have tried to implement it inside server actions (server-side), it did not work and I got a Firebase Error (auth/operation-not-supported-in-this-environment).
To be honest, I would like to handle everything with SSR. If there is a way to do it, I would like to improve it!!
You can setup session cookies and validate them. The requirement is though is that you will need an environment that supports admin sdk or you will implement jwt validation by yourself.
I didn't know that! I’m going to give it a try when I get a chance 🚀
Can we check and prevent unauthorized access to SSR pages? For example, I have a /products page created with server-side rendering that is accessible only to authorized users. I understand that login operations are done on the client side. But other than that I want to protect pages from unauthorized access
I think we can. What about passing ‘/products’ to
protectedRoutes
inmiddleware.ts
?This middleware allows us to run the code before a request is completed. I have not implemented the checking of token validity but it could redirect to the public page we have specified when unauthenticated users try to access protected pages. You might have seen the repo already, but if you define ‘/products’ in
constants.ts
likeHOME_ROUTE
, you can easily use it in the entire app.Middleware in Next.js: nextjs.org/docs/app/building-your-...