Go to home page

Build a Blog Site with Next.js and Firebase Part 4 - Adding Authentication

Welcome to the fourth post in my new "Build a Blog Site with Next.js and Firebase" series! This series is pretty similar to a series I wrote in 2019: "Build a React & Firebase Blog Site". Because it's been well over a year since I published that series, I decided to create a new series and use the Next.js React framework this time. It's a fun framework to use, and I know so many people that are curious about it. I hope you enjoy the series!


The importance of authentication in this app is making it so only you can create, edit, and delete posts. It is your blog after all. We'll use Firebase Authentication to manage a user account and we'll track the authentication state using React Context. Then we can hide certain parts of the app from people who aren't logged in.

Set up Authentication in the Firebase Console

Firebase makes it easy for us to set up email/password authentication in our app. It's also easy to add a user for ourselves right from the console. Make sure you use a real email address and a secure password for your user. This is how you're protecting your blog! There's a video after these steps that visually walks though how to do this.

  1. Go to the Firebase console for your project.
  2. Go to the "Authentication" page under "Build".
  3. Click "Get Started".
  4. You should be on the "Sign-in method" tab.
  5. Click "Email/Password".
  6. Enable the first toggle.
  7. Click "Save".
  8. Switch to the "Users" tab.
  9. Click "Add user".
  10. Enter a secure email and password.
  11. Click "Add user".
<iframe width="100%" height="350" src="https://www.youtube-nocookie.com/embed/WTnIU0ZtCoo" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
  1. Go to your Realtime Database.
  2. Click the "Rules" tab.
  3. Update the rules to allow all reads but only authenticated writes:
{
  "rules": {
    ".read": true,
    ".write": "auth != null"
  }
}
  1. Click the "Publish" button.

Manage Authentication State with React Context

  1. Import firebase/auth into lib/firebase.js after the other Firebase imports:
import 'firebase/auth';
  1. Add an onAuthStateChanged function to lib/firebase.js:
/*
Observes changes in authentication. Receives a callback function that is invoked
when auth state changes. See the Firebase Reference Docs for all of the details:
https://firebase.google.com/docs/reference/js/firebase.auth.Auth#onauthstatechanged
*/
export const onAuthStateChanged = async (callback) => {
  initFirebase();

  return firebase.auth().onAuthStateChanged((user) => callback(user));
};
  1. Add a contexts directory at the root of the project.
  2. Update jsconfig.json for the new contexts directory:
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@components": ["components"],
      "@contexts/*": ["contexts/*"],
      "@lib/*": ["lib/*"],
      "@styles/*": ["styles/*"]
    }
  }
}
  1. Add an auth.js file to contexts:
import { createContext, useState, useEffect, useContext } from 'react';
import { onAuthStateChanged } from '@lib/firebase';

const AuthContext = createContext({ user: null, userLoading: true });

export const AuthProvider = ({ children }) => {
  const [userLoading, setUserLoading] = useState(true);
  const [user, setUser] = useState();

  useEffect(() => {
    return onAuthStateChanged((res) => {
      setUser(res);
      setUserLoading(false);
    });
  }, []);

  return (
    <AuthContext.Provider value={[user, userLoading]}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);
  1. Update pages/_app.js to use AuthProvider:
import { AuthProvider } from '@contexts/auth';
import '@styles/global.scss';

const App = ({ Component, pageProps }) => (
  <AuthProvider>
    <Component {...pageProps} />
  </AuthProvider>
);

export default App;
  1. Restart your development server if it's running.
  2. Make sure your home page loads without error.
  3. Commit and push your work to your repository:
git add .
git commit -m "Adding AuthContext"
git push

Check for an Authenticated User in CreatePage

  1. Import useAuth in pages/create.js:
import { useAuth } from '@contexts/auth';
  1. Invoke it in the component after the isLoading variable and log the results.
const [user, userLoading] = useAuth();
console.log(user, userLoading);
  1. Visit http://localhost:3000/create in your browser.
  2. You should see three logs in the console:
undefined true
null true
null false

The first shows the initial values of user and userLoading when we invoke useAuth. The second shows those values after the callback we pass into onAuthStateChanged has been invoked and setUser has been called. The third shows after setUserLoading has been called in the same callback. This is why we call setUser before setUserLoading, so we can ensure that everything is truly loaded before changing that state.

  1. Redirect to 404 if no user and return null if user is loading, before the handleChange and handleSubmit functions are defined. This placement prevents us from unecessarily defining those two functions if we're not going to use them.
if (userLoading) {
  return null;
}

if (!user && typeof window !== 'undefined') {
  router.push('/404');
  return null;
}
  1. Visit http://localhost:3000/create in your browser.
  2. You should be redirected to the 404 page.
  3. Commit and push your work to your repository:
git add .
git commit -m "Requiring Authentication in CreatePage"
git push

Add a Page for Signing In

  1. Add a new signIn function to lib/firebase.js:
/*
Attempts to authenticate a user with a given email and password.
*/
export const signIn = async (email, password) => {
  initFirebase();

  return firebase.auth().signInWithEmailAndPassword(email, password);
};
  1. Add styles/signin.module.scss:
.SignIn {
  max-width: 500px;
  margin: 32px auto;
  padding: 32px;
  background-color: white;
  border-radius: 4px;
  box-shadow: 0 2px 6px rgba(black, 0.3);

  form {
    h1 {
      margin: 0;
      margin-bottom: 24px;
      text-align: center;
    }

    input {
      margin-bottom: 16px;
    }

    button {
      min-width: 120px;
      display: block;
      margin-top: 32px;
      margin-right: auto;
      margin-left: auto;
    }
  }
}
  1. Add pages/signin.js:
import { useState } from 'react';
import { useRouter } from 'next/router';
import { signIn } from '@lib/firebase';
import { useAuth } from '@contexts/auth';
import styles from '@styles/signin.module.scss';

const SignInPage = () => {
  const router = useRouter();
  const [user, userLoading] = useAuth();
  const [values, setValues] = useState({ email: '', password: '' });

  if (userLoading) {
    return <h1>Loading...</h1>;
  }

  if (user && typeof window !== 'undefined') {
    router.push('/');
    return null;
  }

  const handleChange = (e) => {
    const id = e.target.id;
    const newValue = e.target.value;

    setValues({ ...values, [id]: newValue });
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    let missingValues = [];
    Object.entries(values).forEach(([key, value]) => {
      if (!value) {
        missingValues.push(key);
      }
    });

    if (missingValues.length > 1) {
      alert(`You're missing these fields: ${missingValues.join(', ')}`);
      return;
    }

    signIn(values.email, values.password).catch((err) => {
      alert(err);
    });
  };

  return (
    <div className={styles.SignIn}>
      <form onSubmit={handleSubmit}>
        <h1>Please Sign In</h1>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          value={values.email}
          onChange={handleChange}
        />
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          value={values.password}
          onChange={handleChange}
        />
        <button type="submit">Sign In</button>
      </form>
    </div>
  );
};

export default SignInPage;
  1. Go to http://localhost:3000/signin in your browser.
  2. Try logging in with the user credentials you created at the beginning of this post.
  3. If successful, you should be sent to the home page.
  4. Go to http://localhost:3000/create in your browser. You should not be redirected to the 404 page.
  5. Commit and push your work to your repository:
git add .
git commit -m "Adding SignInPage"
git push

Add a Sign Out Button to Layout

  1. Add a new signOut function to lib/firebase.js:
/*
Signs out the authenticated user.
*/
export const signOut = async () => {
  initFirebase();

  return firebase.auth().signOut();
};
  1. Update the Layout component to check for a user and render a Sign Out button if there is one.
import { signOut } from '@lib/firebase';
import { useAuth } from '@contexts/auth';
import styles from './Layout.module.scss';

const Layout = ({ children }) => {
  const [user] = useAuth();

  return (
    <div className={styles.Layout}>
      <nav>
        <span>
          <a href="/">My Next.js Blog</a>
        </span>
        {user && (
          <span>
            <button onClick={() => signOut()}>Sign Out</button>
          </span>
        )}
      </nav>
      <main>{children}</main>
    </div>
  );
};

export default Layout;
  1. Update the Layout styles so the <nav> element uses Flexbox and the new button stands out from the blue bar.
.Layout {
  nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    ...

    span {
      ...

      button {
        color: #1a73e8;
        background-color: white;
      }
    }
  }

  ...
}
  1. Go to http://localhost:3000/ in your browser.
  2. In the blue bar at the top, you should see a white button with blue text that reads "Sign Out" like this:

View of the Sign Out button.

  1. Click the Sign Out button and check that you can no longer access the create page at http://localhost:3000/create.
  2. Commit and push your work to your repository:
git add .
git commit -m "Adding Sign Out button to Layout"
git push
  1. Celebrate!!! You did it!!! <span role="img" aria-label="party popper emoji">🎉</span>