In this article, you’ll learn how to implement Google OAuth2 in a React.js application, including creating a project in the Google API Console, configuring the application’s client ID and redirect URI, and implementing the necessary code in the React application.

Why should you add Google OAuth2 to your React app? OAuth2 is an open standard for authorization that enables third-party applications to obtain limited access to a user’s resources without sharing their credentials. With Google OAuth2, users can securely log into the React application using their Google account without sharing their password.

Related articles:

How to Implement Google OAuth2 in React.js

Run the React Google OAuth2 Project

  1. Download or clone the React Google OAuth2 project from https://github.com/wpcodevo/google-github-oath2-reactjs and open the source code in an IDE or text editor.
  2. Open the integrated terminal in your IDE and run yarn or yarn install to install the project’s dependencies.
  3. Duplicate the example.env file and rename the duplicated one to .env.local .
  4. Follow the “Get the Google OAuth2 Client ID and Secret” section to obtain the OAuth2 client ID and secret from the Google developer Console API.
  5. Add the OAuth2 client ID and secret to the .env.local file.
  6. Start the Vite development server by running yarn dev .
  7. Set up the backend API to process the request when the Google OAuth2 API redirects the user to the authorised redirect URI.

Run the React app with a Node.js API

  1. Ensure you have Node.js and Yarn installed on your system.
  2. Download or clone the Node.js Google OAuth2 project from https://github.com/wpcodevo/google-github-oauth2-nodejs and open the source code in an IDE.
  3. Open the integrated terminal and change the Git branch to google-oauth2-nodejs . Feel free to use the master branch.
  4. Run yarn or yarn install to install all the required dependencies.
  5. Duplicate the example.env file and rename the copied one to .env .
  6. Add the Google OAuth2 client ID and secret to the .env file.
  7. Run npx prisma db push to push the Prisma schema to the SQLite database.
  8. Start the Express server by running yarn start in the console of the root directory.
  9. Interact with the Node.js API from the React app.

Run the React app with a Golang API

  • Ensure you have the latest version of Golang installed on your system.
  • Download or clone the Golang Google OAuth2 project from https://github.com/wpcodevo/google-github-oauth2-golang and open the source code in a code editor.
  • Change the Git branch to google-oauth2-golang . Feel free to use the master branch instead.
  • Duplicate the example.env file and rename the copied one to app.env .
  • Add the Google OAuth2 client ID and secret to the app.env file.
  • Run go run main.go to install the required dependencies and start the Gin HTTP server.
  • Interact with the Golang API from the React app.

Setup the React Project

To begin, navigate to the location where you would like to create the project and run the command below.


npm create vite@latest google-oauth2-reactjs -- --template react
# or
yarn create vite google-oauth2-reactjs -- --template react

This will generate a React.js boilerplate project with the Vite scaffolding tool and output the files into the google-oauth2-reactjs folder. Feel free to give the project another name.

Once that is done, change into the newly-created project folder, and run yarn or npm install to install the necessary dependencies. After that, open the project folder in a code editor.

Install Tailwind CSS and its peer dependencies with the command below:


npm install -D tailwindcss postcss autoprefixer
# or
yarn add -D tailwindcss postcss autoprefixer

Run the Tailwind CSS init command to generate the tailwind.config.cjs and postcss.config.cjs files.


npx tailwindcss init -p

Open the tailwind.config.cjs file and replace its content with the following Tailwind CSS configurations.

tailwind.config.cjs


/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        'ct-dark-600': '#222',
        'ct-dark-200': '#e5e7eb',
        'ct-dark-100': '#f5f6f7',
        'ct-blue-600': '#2363eb',
        'ct-yellow-600': '#f9d13e',
      },
      fontFamily: {
        Poppins: ['Poppins, sans-serif'],
      },
      container: {
        center: true,
        padding: '1rem',
        screens: {
          lg: '1125px',
          xl: '1125px',
          '2xl': '1125px',
        },
      },
    },
  },
  plugins: [],
};


Next, open the src/index.css file and replace its content with the following CSS and Tailwind CSS directives.

src/index.css


@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

html{
    font-family: 'Poppins', sans-serif;
}

Now let’s install the dependencies we’ll need for the project:


yarn add react-hook-form @hookform/resolvers react-router-dom react-toastify tailwind-merge zod zustand
# or 
npm install react-hook-form @hookform/resolvers react-router-dom react-toastify tailwind-merge zod zustand

After the installation is complete, open the package.json file and replace the dev script with "dev": "vite --port 3000" . This will start the Vite development server on port 3000 instead of the default port 5173.

Get the Google OAuth2 Client ID and Secret

  1. Make sure you’ve already logged into your Google account and go to the Google API Console.
  2. Click on the dropdown adjacent to the Google Cloud logo to display a popup where you can select an existing project or create a new one.
    select a project or create a new one on the Google Cloud API dashboard
  3. To create a new project, click on the “New Project” button at the top-right corner of the popup. On the next page, provide the project name and click on the “Create” button.
    create a new project on the google console api dashboard
  4. Within a few milliseconds, the project will be created and a notification will be displayed for you to select the newly-created project.
    click on the newly created project from the notification
    From the “Notifications“, click on the “SELECT PROJECT” button available on the newly-created project.
  5. In the left sidebar, click on the “OAuth consent screen” menu and select “External” under the “User Type” on the next page.
    select external under the user type and click on create
    After that, click on the “Create” button.
  6. Under the “App information” on the “Edit app registration” screen, provide the required consent screen information.
    provide the consent screen credentials part 1
    Scroll down to the “App domain” section, and provide the application links.
    provide the consent screen credentials part 2
    Under the “Developer contact information” section, enter your email and click on the “SAVE AND CONTINUE” button.
  7. On the “Scopes” screen, click on the “ADD OR REMOVE SCOPES” button, select .../auth/userinfo.email and .../auth/userinfo.profile , and click on the “UPDATE” button at the bottom.
    select the scopes
    On the “Scopes” screen, scroll down and click on the “SAVE AND CONTINUE” button.
  8. Click on the “ADD USERS” button on the “Test users” screen. Only the test users can log into your application while still in sandbox mode.
    add the test user
    After adding the test users, click on the “SAVE AND CONTINUE” button and on the “Summary” screen, click on the “BACK TO DASHBOARD” button.
  9. Now that we’ve set up the consent screen, we can go ahead and create the credentials. In the left sidebar, click on the “Credentials” menu and click on the “CREATE CREDENTIALS” button. Select “OAuth client ID” from the options.
    select oauth client ID
  10. On the “Create OAuth client ID” screen, select “Web application” as the Application type, enter the app name, and provide the authorized redirect URI.
    provide the oauth credentials
    Enter http://localhost:8000/api/sessions/oauth/google as the Authorised redirect URI and click on the “Create” button at the bottom.
  11. Create a .env.local file in the root directory, add the Client ID as VITE_GOOGLE_OAUTH_CLIENT_ID. Also, add the Client secret as VITE_GOOGLE_OAUTH_CLIENT_SECRET .

In the end, your .env.local file should look somewhat like this:


VITE_SERVER_ENDPOINT=http://localhost:8000

VITE_GOOGLE_OAUTH_CLIENT_ID=
VITE_GOOGLE_OAUTH_CLIENT_SECRET=
VITE_GOOGLE_OAUTH_REDIRECT=http://localhost:8000/api/sessions/oauth/google

Build the OAuth2 Consent Screen Link

Now that we’ve obtained the OAuth2 client ID and secret from the Google Console API, let’s create a helper function to generate the consent screen URL from them.

To do this, create a utils folder in the src directory. Within the src/utils/ folder, create a getGoogleUrl.ts file and add the following code.

src/utils/getGoogleUrl.ts


export const getGoogleUrl = (from: string) => {
  const rootUrl = `https://accounts.google.com/o/oauth2/v2/auth`;

  const options = {
    redirect_uri: import.meta.env.VITE_GOOGLE_OAUTH_REDIRECT as string,
    client_id: import.meta.env.VITE_GOOGLE_OAUTH_CLIENT_ID as string,
    access_type: "offline",
    response_type: "code",
    prompt: "consent",
    scope: [
      "https://www.googleapis.com/auth/userinfo.profile",
      "https://www.googleapis.com/auth/userinfo.email",
    ].join(" "),
    state: from,
  };

  const qs = new URLSearchParams(options);

  return `${rootUrl}?${qs.toString()}`;
};

In the above code, we created a function to generate the consent screen URL based on the OAuth2 credentials.

  • client_id – Is the client ID of the application, which we obtained from the Google API Console.
  • redirect_uri – This is the URL the OAuth2 API will redirect the user to after the permission has been granted or denied. This URL must match one of the redirect URLs configured in the Google API Console.
  • access_type: "offline" – This indicates that the application needs to access the user’s data when the user is not present.
  • scope – This is a scope of permissions the application is requesting. In this project, we’ll only request access to the user’s email address and profile information.
  • response_type: "code" – This indicates that an authorization code will be returned on the query string.
  • state – This will allow us to pass data to the backend API. In this example, we’ll include a path in the state so that the backend API can redirect the user to that page after the authentication is successful.
  • prompt: "consent" – This indicates that the user should only see the consent screen the first time they grant permission to the application.

With that out of the way, we can now evoke the getGoogleUrl("/profile") function in an href attribute of a link tag to redirect the user to the consent screen when the link is clicked. Don’t worry, we’ll later do this in the login component.


    <a href={getGoogleUrl(from)}>
              <img
                className="pr-2"
                src={GoogleLogo}
                alt=""
                style={{ height: "2rem" }}
              />
              Continue with Google
         </a>

Create a Zustand Store

Now let’s set up a global state store using Zustand. This way, we can store the authenticated user’s credentials in the store and display them when needed.

To begin, create a store folder in the src directory. In the store folder, create a types.ts file and add the type below. The IUser type will describe the structure of a user record that the backend API will return.

src/store/types.ts


export interface IUser {
  id: string;
  name: string;
  email: string;
  role: string;
  photo: string;
  provider: string;
  verified: boolean;
}


Next, create an index.ts file in the store folder and add the code below:

src/store/index.ts


import { create } from "zustand";
import { IUser } from "./types";

type Store = {
  authUser: IUser | null;
  requestLoading: boolean;
  setAuthUser: (user: IUser | null) => void;
  setRequestLoading: (isLoading: boolean) => void;
};

const useStore = create<Store>((set) => ({
  authUser: null,
  requestLoading: false,
  setAuthUser: (user) => set((state) => ({ ...state, authUser: user })),
  setRequestLoading: (isLoading) =>
    set((state) => ({ ...state, requestLoading: isLoading })),
}));

export default useStore;

In the above file, we defined a type for the state and the actions that will be used to update the state. Then, we used the create method to create the store with some initial states and actions.

Next, we exported the useStore hook from the file. The useStore hook will allow us to access and mutate the store from other React components.

Create Reusable React Components

Before we can implement the Google OAuth2 flow in the React app, we first need to create some reusable components.

Create a Spinner Component

The first component is a Spinner that will be displayed when a request is being processed by the backend API. Create a src/components/Spinner.tsx file and add the following TSX code:

src/components/Spinner.tsx


import React from 'react';
import { twMerge } from 'tailwind-merge';
type SpinnerProps = {
  width?: number;
  height?: number;
  color?: string;
  bgColor?: string;
};
const Spinner: React.FC<SpinnerProps> = ({
  width = 5,
  height = 5,
  color,
  bgColor,
}) => {
  return (
    <svg
      role='status'
      className={twMerge(
        'w-5 h-5 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600',
        `w-${width} h-${height} ${color} ${bgColor}`
      )}
      viewBox='0 0 100 101'
      fill='none'
      xmlns='http://www.w3.org/2000/svg'
    >
      <path
        d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z'
        fill='currentColor'
      />
      <path
        d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z'
        fill='currentFill'
      />
    </svg>
  );
};

export default Spinner;

Create a Header Component

The second component will be the Header which will display a list of links for navigating through the different pages. Also, this component will hold the logic for logging out of the application.

Create a Header.tsx file in the src/components folder and add the following TSX code.

src/components/Header.tsx


import { Link, useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import useStore from "../store";
import Spinner from "./Spinner";

const Header = () => {
  const store = useStore();
  const user = store.authUser;
  const navigate = useNavigate();

  const handleLogout = async () => {
    try {
      store.setRequestLoading(true);
      const VITE_SERVER_ENDPOINT = import.meta.env.VITE_SERVER_ENDPOINT;
      const response = await fetch(`${VITE_SERVER_ENDPOINT}/api/auth/logout`, {
        credentials: "include",
      });
      if (!response.ok) {
        throw await response.json();
      }

      store.setRequestLoading(false);
      store.setAuthUser(null);
      navigate("/login");
    } catch (error: any) {
      store.setRequestLoading(false);
      const resMessage =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();

      if (error?.message === "You are not logged in") {
        navigate("/login");
      }

      toast.error(resMessage, {
        position: "top-right",
      });
    }
  };

  return (
    <>
      <header className="bg-white h-20">
        <nav className="h-full flex justify-between container items-center">
          <div>
            <Link to="/" className="text-ct-dark-600 text-2xl font-semibold">
              CodevoWeb
            </Link>
          </div>
          <ul className="flex items-center gap-4">
            <li>
              <Link to="/" className="text-ct-dark-600">
                Home
              </Link>
            </li>
            {!user && (
              <>
                <li>
                  <Link to="/login" className="text-ct-dark-600">
                    Login
                  </Link>
                </li>
                <li>
                  <Link to="/register" className="text-ct-dark-600">
                    Register
                  </Link>
                </li>
              </>
            )}
            {user && (
              <>
                <li>
                  <Link to="/profile" className="text-ct-dark-600">
                    Profile
                  </Link>
                </li>
                <li className="cursor-pointer" onClick={handleLogout}>
                  Logout
                </li>
              </>
            )}
          </ul>
        </nav>
      </header>
      <div className="pt-4 pl-2 bg-ct-blue-600 fixed">
        {store.requestLoading && <Spinner color="text-ct-yellow-600" />}
      </div>
    </>
  );
};

export default Header;

Create a Layout Component

Instead of adding the Header component to every page that needs it, let’s use the power of react-router-dom to create a layout component that will render the pages below the Header component. To achieve this, we’ll leverage React-Router-Dom’s Outlet component.

Create a Layout.tsx file in the src/components directory and add the code below.

src/components/Layout.tsx


import { Outlet } from "react-router-dom";
import Header from "./Header";

const Layout = () => {
  return (
    <>
      <Header />
      <Outlet />
    </>
  );
};

export default Layout;

Implement the Authentication

At this point, we’re now ready to implement the JWT authentication and OAuth2 flow in the React project. To do that, we’ll create two routes:

  • /register – This route will display the account registration page where the user can create a new account using the email, password, and name.
  • /login – This route will display the account login page where the user can either sign in with the OAuth2 option or email and password.

Account Registration Page

Here, you’ll create a React component that will have the logic for registering new users. To prevent the user from sending junk credentials to the backend, we’ll use the React-Hook-Form library along with the Zod schema validation library to validate the credentials before sending the request to the backend API.

src/pages/register.page.tsx


import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import useStore from "../store";
import { object, string, TypeOf } from "zod";
import { useEffect } from "react";
import { useForm, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const registerSchema = object({
  name: string().min(1, "Full name is required").max(100),
  email: string()
    .min(1, "Email address is required")
    .email("Email Address is invalid"),
  password: string()
    .min(1, "Password is required")
    .min(8, "Password must be more than 8 characters")
    .max(32, "Password must be less than 32 characters"),
  passwordConfirm: string().min(1, "Please confirm your password"),
}).refine((data) => data.password === data.passwordConfirm, {
  path: ["passwordConfirm"],
  message: "Passwords do not match",
});

export type RegisterInput = TypeOf<typeof registerSchema>;

const RegisterPage = () => {
  const navigate = useNavigate();
  const store = useStore();

  const registerUser = async (data: RegisterInput) => {
    try {
      store.setRequestLoading(true);
      const VITE_SERVER_ENDPOINT = import.meta.env.VITE_SERVER_ENDPOINT;
      const response = await fetch(
        `${VITE_SERVER_ENDPOINT}/api/auth/register`,
        {
          method: "POST",
          credentials: "include",
          body: JSON.stringify(data),
          headers: {
            "Content-Type": "application/json",
          },
        }
      );
      if (!response.ok) {
        throw await response.json();
      }

      toast.success("Account created successfully", {
        position: "top-right",
      });
      store.setRequestLoading(false);
      navigate("/login");
    } catch (error: any) {
      store.setRequestLoading(false);
      if (error.error) {
        error.error.forEach((err: any) => {
          toast.error(err.message, {
            position: "top-right",
          });
        });
        return;
      }
      const resMessage =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();

      toast.error(resMessage, {
        position: "top-right",
      });
    }
  };

  const methods = useForm<RegisterInput>({
    resolver: zodResolver(registerSchema),
  });

  const {
    reset,
    handleSubmit,
    register,
    formState: { isSubmitSuccessful, errors },
  } = methods;

  useEffect(() => {
    if (isSubmitSuccessful) {
      reset();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isSubmitSuccessful]);

  const onSubmitHandler: SubmitHandler<RegisterInput> = (values) => {
    registerUser(values);
  };

  return (
    <section className="bg-ct-blue-600 min-h-screen pt-20">
      <div className="container mx-auto px-6 py-12 h-full flex justify-center items-center">
        <div className="md:w-8/12 lg:w-5/12 bg-white px-8 py-10">
          <form onSubmit={handleSubmit(onSubmitHandler)}>
            <div className="mb-6">
              <input
                type="text"
                className="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
                placeholder="Name"
                {...register("name")}
              />
              {errors.name && (
                <p className="text-red-700 text-sm mt-1">
                  {errors.name?.message}
                </p>
              )}
            </div>
            <div className="mb-6">
              <input
                type="email"
                className="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
                placeholder="Email address"
                {...register("email")}
              />
              {errors.email && (
                <p className="text-red-700 text-sm mt-1">
                  {errors.email?.message}
                </p>
              )}
            </div>

            <div className="mb-6">
              <input
                type="password"
                className="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
                placeholder="Password"
                {...register("password")}
              />
              {errors.password && (
                <p className="text-red-700 text-sm mt-1">
                  {errors.password?.message}
                </p>
              )}
            </div>

            <div className="mb-6">
              <input
                type="password"
                className="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
                placeholder="Confirm Password"
                {...register("passwordConfirm")}
              />
              {errors.passwordConfirm && (
                <p className="text-red-700 text-sm mt-1">
                  {errors.passwordConfirm?.message}
                </p>
              )}
            </div>

            <button
              type="submit"
              className="inline-block px-7 py-4 bg-blue-600 text-white font-medium text-sm leading-snug uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out w-full"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
            >
              Sign up
            </button>
          </form>
        </div>
      </div>
    </section>
  );
};

export default RegisterPage;

Login Page

Now it’s time to implement the OAuth2 logic. In addition, the user will have the option to log in with an email and password. Let me clear the air. Users who register with OAuth2 won’t be able to use their Google account email and password to log into the application. They can only log into the application using the OAuth option.

To begin, you’ll need access to the GitHub and Google SVG logos. Click on this link to download the assets folder as a Zip file. Unzip the file and move it to the src folder to replace the existing assets folder.

Next, create a login.page.tsx file in the src/pages/ directory and add the following TSX code.

src/pages/login.page.tsx


import { useLocation, useNavigate } from "react-router-dom";
import GitHubLogo from "../assets/github.svg";
import GoogleLogo from "../assets/google.svg";
import { getGoogleUrl } from "../utils/getGoogleUrl";
import { object, string, TypeOf } from "zod";
import { useForm, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import useStore from "../store";
import { toast } from "react-toastify";
import { useEffect } from "react";

const loginSchema = object({
  email: string()
    .min(1, "Email address is required")
    .email("Email Address is invalid"),
  password: string()
    .min(1, "Password is required")
    .min(8, "Password must be more than 8 characters")
    .max(32, "Password must be less than 32 characters"),
});

export type LoginInput = TypeOf<typeof loginSchema>;

const LoginPage = () => {
  const location = useLocation();
  const navigate = useNavigate();
  const store = useStore();
  const from = ((location.state as any)?.from.pathname as string) || "/profile";

  const loginUser = async (data: LoginInput) => {
    try {
      store.setRequestLoading(true);
      const VITE_SERVER_ENDPOINT = import.meta.env.VITE_SERVER_ENDPOINT;
      const response = await fetch(`${VITE_SERVER_ENDPOINT}/api/auth/login`, {
        method: "POST",
        credentials: "include",
        body: JSON.stringify(data),
        headers: {
          "Content-Type": "application/json",
        },
      });
      if (!response.ok) {
        throw await response.json();
      }

      store.setRequestLoading(false);
      navigate("/profile");
    } catch (error: any) {
      store.setRequestLoading(false);
      if (error.error) {
        error.error.forEach((err: any) => {
          toast.error(err.message, {
            position: "top-right",
          });
        });
        return;
      }
      const resMessage =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();

      toast.error(resMessage, {
        position: "top-right",
      });
    }
  };

  const methods = useForm<LoginInput>({
    resolver: zodResolver(loginSchema),
  });

  const {
    reset,
    handleSubmit,
    register,
    formState: { isSubmitSuccessful, errors },
  } = methods;

  useEffect(() => {
    if (isSubmitSuccessful) {
      reset();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isSubmitSuccessful]);

  const onSubmitHandler: SubmitHandler<LoginInput> = (values) => {
    loginUser(values);
  };

  return (
    <section className="bg-ct-blue-600 min-h-screen pt-20">
      <div className="container mx-auto px-6 py-12 h-full flex justify-center items-center">
        <div className="md:w-8/12 lg:w-5/12 bg-white px-8 py-10">
          <form onSubmit={handleSubmit(onSubmitHandler)}>
            <div className="mb-6">
              <input
                type="email"
                className="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
                placeholder="Email address"
                {...register("email")}
              />
              {errors.email && (
                <p className="text-red-700 text-sm mt-1">
                  {errors.email?.message}
                </p>
              )}
            </div>

            <div className="mb-6">
              <input
                type="password"
                className="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
                placeholder="Password"
                {...register("password")}
              />

              {errors.password && (
                <p className="text-red-700 text-sm mt-1">
                  {errors.password?.message}
                </p>
              )}
            </div>

            <div className="flex justify-between items-center mb-6">
              <div className="form-group form-check">
                <input
                  type="checkbox"
                  className="form-check-input appearance-none h-4 w-4 border border-gray-300 rounded-sm bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer"
                  id="exampleCheck3"
                />
                <label
                  className="form-check-label inline-block text-gray-800"
                  htmlFor="exampleCheck2"
                >
                  Remember me
                </label>
              </div>
              <a
                href="#!"
                className="text-blue-600 hover:text-blue-700 focus:text-blue-700 active:text-blue-800 duration-200 transition ease-in-out"
              >
                Forgot password?
              </a>
            </div>

            <button
              type="submit"
              className="inline-block px-7 py-4 bg-blue-600 text-white font-medium text-sm leading-snug uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out w-full"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
            >
              Sign in
            </button>

            <div className="flex items-center my-4 before:flex-1 before:border-t before:border-gray-300 before:mt-0.5 after:flex-1 after:border-t after:border-gray-300 after:mt-0.5">
              <p className="text-center font-semibold mx-4 mb-0">OR</p>
            </div>

            <a
              className="px-7 py-2 text-white font-medium text-sm leading-snug uppercase rounded shadow-md hover:shadow-lg focus:shadow-lg focus:outline-none focus:ring-0 active:shadow-lg transition duration-150 ease-in-out w-full flex justify-center items-center mb-3"
              style={{ backgroundColor: "#3b5998" }}
              href={getGoogleUrl(from)}
              role="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
            >
              <img
                className="pr-2"
                src={GoogleLogo}
                alt=""
                style={{ height: "2rem" }}
              />
              Continue with Google
            </a>
            <a
              className="px-7 py-2 text-white font-medium text-sm leading-snug uppercase rounded shadow-md hover:shadow-lg focus:shadow-lg focus:outline-none focus:ring-0 active:shadow-lg transition duration-150 ease-in-out w-full flex justify-center items-center"
              style={{ backgroundColor: "#55acee" }}
              href="#!"
              role="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              onClick={() => alert("Only Google OAuth2 is implemented")}
            >
              <img
                className="pr-2"
                src={GitHubLogo}
                alt=""
                style={{ height: "2.2rem" }}
              />
              Continue with GitHub
            </a>
          </form>
        </div>
      </div>
    </section>
  );
};

export default LoginPage;

Once the local or OAuth2 login is successful, the backend API will send an HTTP Only cookie that the React app will include in subsequent requests to access protected routes.

Also, the user will be redirected to the profile page where the account credentials will be displayed in the UI.

Create the Remaining Pages

So far so good. Let’s create the remaining React components. Here, you’ll create Home and Profile pages.

Home Page

The home page will display a simple message when the user lands on the root route of the React application. Create a home.page.tsx file in the src/pages/ directory and add the code below.

src/pages/home.page.tsx


const HomePage = () => {
  return (
    <>
      <section className="bg-ct-blue-600 min-h-screen pt-20">
        <div className="max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center">
          <p className="text-3xl font-semibold">
            Welcome to Google OAuth2 with React.js
          </p>
        </div>
      </section>
    </>
  );
};

export default HomePage;

Profile Page

When the backend API redirects the user to this page, a GET request will be fired to retrieve the authenticated user’s profile information. For this to work, the React app will send along the JWT token to retrieve the user’s credentials.

Once the request resolves successfully, React will re-render the DOM to display the user’s account information in the UI.

src/pages/profile.page.tsx


import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import useStore from "../store";
import { IUser } from "../store/types";

const ProfilePage = () => {
  const navigate = useNavigate();
  const store = useStore();

  const fetchUser = async () => {
    try {
      store.setRequestLoading(true);
      const VITE_SERVER_ENDPOINT = import.meta.env.VITE_SERVER_ENDPOINT;
      const response = await fetch(`${VITE_SERVER_ENDPOINT}/api/users/me`, {
        credentials: "include",
      });
      if (!response.ok) {
        throw await response.json();
      }

      const data = await response.json();
      const user = data.data.user as IUser;
      store.setRequestLoading(false);
      console.log(user);

      store.setAuthUser(user);
    } catch (error: any) {
      store.setRequestLoading(false);
      if (error.error) {
        error.error.forEach((err: any) => {
          toast.error(err.message, {
            position: "top-right",
          });
        });
        return;
      }
      const resMessage =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();

      if (error?.message === "You are not logged in") {
        navigate("/login");
      }

      toast.error(resMessage, {
        position: "top-right",
      });
    }
  };

  useEffect(() => {
    fetchUser();
  }, []);

  const user = store.authUser;

  return (
    <section className="bg-ct-blue-600  min-h-screen pt-20">
      <div className="max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center">
        <div>
          <p className="text-5xl text-center font-semibold">Profile Page</p>
          {!user ? (
            <p>Loading...</p>
          ) : (
            <div className="flex items-center gap-8">
              <div>
                <img
                  src={
                    user.photo.includes("default.png")
                      ? `http://localhost:8000/api/images/${user.photo}`
                      : user.photo
                  }
                  className="max-h-36"
                  alt={`profile photo of ${user.name}`}
                />
              </div>
              <div className="mt-8">
                <p className="mb-3">ID: {user.id}</p>
                <p className="mb-3">Name: {user.name}</p>
                <p className="mb-3">Email: {user.email}</p>
                <p className="mb-3">Role: {user.role}</p>
                <p className="mb-3">Provider: {user.provider}</p>
              </div>
            </div>
          )}
        </div>
      </div>
    </section>
  );
};

export default ProfilePage;

Create Routes for the Pages

Now that we’ve created all the page components, let’s create routes for them. To do this, we’ll use the object syntax provided by React-Router-Dom v6.

Create a router folder in the src directory. Within the src/router folder, create a index.tsx file and add the following code.

src/router/index.tsx


import type { RouteObject } from "react-router-dom";
import Layout from "../components/Layout";
import HomePage from "../pages/home.page";
import LoginPage from "../pages/login.page";
import ProfilePage from "../pages/profile.page";
import RegisterPage from "../pages/register.page";

const normalRoutes: RouteObject = {
  path: "*",
  element: <Layout />,
  children: [
    {
      index: true,
      element: <HomePage />,
    },
    {
      path: "profile",
      element: <ProfilePage />,
    },
    {
      path: "login",
      element: <LoginPage />,
    },
    {
      path: "register",
      element: <RegisterPage />,
    },
  ],
};

const routes: RouteObject[] = [normalRoutes];

export default routes;

Let’s use the useRoutes hook to render the Route Object as Route elements. Open the src/App.tsx file and replace its content with the following code.

src/App.tsx


import { useRoutes } from "react-router-dom";
import routes from "./router";

function App() {
  const content = useRoutes(routes);
  return content;
}

export default App;

For the routing to work, we need to wrap the BrowserRouter component around the entry point of our application. To do this, open the src/main.tsx file and replace its content with the code below.

src/main.tsx


import "./index.css";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
      <ToastContainer />
    </BrowserRouter>
  </React.StrictMode>
);

Oops, quite a lot of code. Am proud of you if you made it this far. Now you can start the Vite development server by running yarn dev .

Test the Google OAuth2 Flow

At this point, we’ve basically implemented the Google OAuth2 flow in the React application. Now we need to test the app with a backend API that has OAuth2 support.

Luckily, I’ve included step-by-step instructions on how you can spin up a Node.js or Golang API that has OAuth2 support. You can find the instruction at the beginning of the article.

Create an Account

Despite having the option to log in with OAuth2, you can log in with an email and a password. Before you’ll be able to use your email and password to log into the application, you need to register for an account.

On the account registration page, provide the required credentials and click on the “SIGN UP” button to submit the form data to the backend API.

Google OAuth2 in React.js Register for an Account

The backend API will validate the credentials, add them to the database, and return a success message to the React app.

Once the request resolves successfully, React will redirect you to the login page where you’ll be required to provide the email and password.

Login with OAuth2

On the login page, you can provide the email and password to sign into the application. Alternatively, you can click on the “CONTINUE WITH GOOGLE” button.

Google OAuth2 in React.js Login

React will redirect you to the Google OAuth2 consent screen where you’ll be prompted to choose a Google account to grant the requested permissions to the application.

Google OAuth2 Consent Screen

From the available Google accounts, click on the test user you configured on the Google API Console to grant the permissions to the React application.

Once you grant the permissions, the Google OAuth2 API will redirect you to the application’s redirect URI with an authorization code as a query parameter. The backend API will then make a POST request to the Google OAuth2 API to exchange the authorization code for an access token. Also, the client ID and client secret will be included in the request.

After the Google OAuth2 API returns the access token, the backend API will make a GET request with the access token, client ID and client secret to obtain the user’s account information.

If the request is successful, the user’s information will be stored in the database and a JWT token will be generated and returned to the frontend app as HTTP Only cookie.

Also, the backend API will redirect the user to the URL path we stored in the state query parameter. In this case, we stored the path to the Profile page.

Access the Protected Page

When the backend API redirects the authenticated user to the Profile page, a GET request will be fired to retrieve the user’s information. For this to work, the React app will include the Cookie along with the request.

After that, the DOM will be re-rendered to display the profile information in the UI.

Google OAuth2 in React.js Access a Protected Page

Conclusion

And we’re done! You can find the complete code of the React Google OAuth2 project on GitHub.

In this tutorial, we implemented Google OAuth2 flow in a React.js application. Our app has all the required functionalities, for example, registering new users, logging them into their account with the OAuth2 option or password, and logging them out of the application.

We even went a step further to create a project in the Google API Console, and obtain the OAuth2 client ID and secret. I hope you enjoyed this article. Don’t forget to leave a comment if you have any questions.