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 GitHub OAuth in React.js
- GitHub OAuth Authentication Vue.js and Node.js (No Passport)
- Google OAuth Authentication React.js and Node.js(No Passport)
- GitHub OAuth Authentication React.js and Node.js(No Passport)
- GitHub OAuth Authentication Vuejs, MongoDB and Golang
- Google OAuth Authentication React.js, MongoDB and Golang
Run the React Google OAuth2 Project
- 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.
- Open the integrated terminal in your IDE and run
yarn
oryarn install
to install the project’s dependencies. - Duplicate the
example.env
file and rename the duplicated one to.env.local
. - 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.
- Add the OAuth2 client ID and secret to the
.env.local
file. - Start the Vite development server by running
yarn dev
. - 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
- Ensure you have Node.js and Yarn installed on your system.
- 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.
- Open the integrated terminal and change the Git branch to
google-oauth2-nodejs
. Feel free to use the master branch. - Run
yarn
oryarn install
to install all the required dependencies. - Duplicate the
example.env
file and rename the copied one to.env
. - Add the Google OAuth2 client ID and secret to the
.env
file. - Run
npx prisma db push
to push the Prisma schema to the SQLite database. - Start the Express server by running
yarn start
in the console of the root directory. - 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 toapp.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
react-hook-form
– Form validation library for React@hookform/resolvers
– React-Hook-Form validation resolverreact-router-dom
– Provides routing for React web applicationsreact-toastify
– Display alert notifications in Reacttailwind-merge
– Merge Tailwind CSS classes without style conflictszod
– A TypeScript-first schema validation libraryzustand
– A state manager for React
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
- Make sure you’ve already logged into your Google account and go to the Google API Console.
- 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.
- 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.
- Within a few milliseconds, the project will be created and a notification will be displayed for you to select the newly-created project.
From the “Notifications“, click on the “SELECT PROJECT” button available on the newly-created project. - In the left sidebar, click on the “OAuth consent screen” menu and select “External” under the “User Type” on the next page.
After that, click on the “Create” button. - Under the “App information” on the “Edit app registration” screen, provide the required consent screen information.
Scroll down to the “App domain” section, and provide the application links.
Under the “Developer contact information” section, enter your email and click on the “SAVE AND CONTINUE” button. - 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.
On the “Scopes” screen, scroll down and click on the “SAVE AND CONTINUE” button. - 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.
After adding the test users, click on the “SAVE AND CONTINUE” button and on the “Summary” screen, click on the “BACK TO DASHBOARD” button. - 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.
- On the “Create OAuth client ID” screen, select “Web application” as the Application type, enter the app name, and provide the authorized redirect URI.
Enterhttp://localhost:8000/api/sessions/oauth/google
as the Authorised redirect URI and click on the “Create” button at the bottom. - Create a
.env.local
file in the root directory, add the Client ID asVITE_GOOGLE_OAUTH_CLIENT_ID
. Also, add the Client secret asVITE_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 thestate
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.
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.
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.
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.
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.
Very helpful. It’s amazing when finding your individual blog.
I’m glad you find individual articles on specific concepts helpful! It’s always great to hear that the content resonates with readers.
This is very kind tutorial.
But I have a question.
I can’t understand about this url
http://localhost:8000/api/sessions/oauth/google
this is node server?
I want to hear from you soon.
thx
You’re absolutely right. The
http://localhost:8000/api/sessions/oauth/google
URL serves as the callback URL that Google uses to redirect the user after granting the frontend application access to their Google account information. In this particular case, this URL points to an endpoint on the Node.js server.Once the Node server receives a request on this endpoint, it extracts a unique code appended to the URL by the Google Authorization API. This code plays a crucial role in the subsequent steps of the authentication process. The server then utilizes this code to acquire access and refresh tokens from the Google OAuth token endpoint.
If the request for tokens is successful, the Node server proceeds to make another request using the access token. This request is made to obtain the user’s Google account information. Once retrieved, the server performs an upsert operation, which involves creating a new user entry or updating an existing one in the database.
In addition to that, the server authenticates the user by generating a JWT (JSON Web Token), which the user can utilize to access protected routes on the server.
I understand that you may be wondering why we don’t handle all of these processes within the React application. However, it’s important to note that these processes involve sensitive information that needs to be handled on the server. Handling sensitive information on the server allows us to implement robust security measures to protect the data involved in the authentication and authorization flow.
I hope this explanation helps to clarify the purpose and flow of the
http://localhost:8000/api/sessions/oauth/google
URL in the context of the application.Can you share the code for the server as well please?
In the article, I have provided detailed instructions in two sections on how to run both a Node.js server and a Golang server alongside the React app.
To access the code and follow the steps, please refer to the sections titled “Run the React app with a Golang API” and “Run the React app with a Node.js API“.