This tutorial will build upon the previous authentication setup to integrate Single Sign-On (SSO) login and registration using Google Oauth. This will allow users to easily sign in using their existing social media accounts, simplifying the signup process and enhancing user experience.
Setting up Google OAuth
First, you must create a project in the Google Cloud Console. Navigate to "APIs & Services" -> "OAuth consent screen". Fill out the necessary information, including your app's name and authorized redirect URIs (these will be specific to your Next.js application). Make sure to specify the "Google People API" to access user profile information.
Next, create credentials. Go to "Credentials" and create an OAuth 2.0 client ID. Select "Web application" as the application type. Enter your authorized redirect URIs. You'll receive a client_id
and client_secret
. Keep these safe and secure; they will be used in your Next.js application.
Remember to add your GOOGLE_CLIENT_ID
and GOOGLE_CLIENT_SECRET
to your environment variables. Add the codes in a .env
file in the root folder of your application. Notice that the client ID needs the NEXT_PUBLIC prefix since browsers need access to it to allow redirects.
<!-- .env -->
NEXT_PUBLIC_GOOGLE_CLIENT_ID="Your Client Id Here"
GOOGLE_CLIENT_SECRET="Your client secret"
Next.js Implementation (Google)
For this tutorial, we will be using the npm package @react-oauth/google from Google which comes with some tools for logging users and even a Google login button.
Begin by installing the package:
npm i @react-oauth/google
The next step is to add a Google provider to our app layout file so Google auth can work properly. I always like defining the provider in a separate file so I can easily modify it or add more providers with time.
Begin by creating a folder name providers
and adding an index.ts
file. This will serve as our context provider in the routes that need Google login.
"use client";
import { GoogleOAuthProvider } from "@react-oauth/google";
export function GoogleContextProvider({
children,
}: {
children: React.ReactNode;
}) {
return (
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!}>
{children}
</GoogleOAuthProvider>
);
}
Next, we need to add the provider to the root layout of our app. In the folderlayout.tsx
, import the provider and add it as follows:
import { GoogleContextProvider } from "@/providers";
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang= "en" ><body>
<GoogleContextProvider>
<main>{ children } < /main>
< /GoogleContextProvider>
< /body>< /html>
);
}
Now we are ready to make use of Google OAuth features in our application. The login flow requires the user to authenticate and receive a response token which we exchange with Google for an access token that contains user information. To make things easier, I created a folder named Google and added the Google login buttons with add functionality.
"use client";
import { useGoogleLogin, useGoogleOneTapLogin } from "@react-oauth/google";
import { toast } from "sonner";
import { GoogleIcon } from "@/assets";
import { authenticateSSOLogin } from "@/lib/actions/user-actions/sso";
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { useRouter } from "next/navigation";
type Props = {
router: AppRouterInstance;
origin_url: string;
};
const GoogleLoginButton = ({ router, origin_url }: Props) => {
const handleGoogleLogin = useGoogleLogin({
flow: "implicit",
onSuccess: (tokenResponse) => {
loginGoogleUsers(tokenResponse.access_token);
},
onError: (error) => {
console.error("Login Failed:", error);
toast.error(error.error_description || "Something went wrong");
},
});
async function loginGoogleUsers(access_token: string) {
try {
const response = await fetch(
"https://www.googleapis.com/oauth2/v3/userinfo",
{
headers: {
Authorization: `Bearer ${access_token}`,
},
}
);
const userInfo = await response.json();
const result = await authenticateSSOLogin(userInfo.email);
if (result.success) {
toast.success("Logged in successfully", {
position: "bottom-center",
});
router.replace(origin_url);
} else {
if (result.error === "User not found") {
toast.error(result.error);
router.replace(
`/login/account_not_found?referrer=google&token=${access_token}`
);
}
toast.error(result.error);
return false;
}
} catch (error: any) {
console.error(error);
toast.error(error.message || "something went wrong");
}
}
return (
<button
className= "rounded-md text-base font-medium border hover:bg-gray-200 hover:text-black h-10 px-4 py-2 w-full flex justify-center gap-4 items-center space-x-2"
type = "button"
onClick = {() => handleGoogleLogin()}>
<GoogleIcon />
< span > Sign in with Google < /span>
< /button>
);
};
export default GoogleLoginButton;
export function GoogleOneTapLogin({ session }: { session: any | null }) {
const router = useRouter();
useGoogleOneTapLogin({
onSuccess: async (credentialResponse) => {
try {
const response = await fetch(
`https://oauth2.googleapis.com/tokeninfo?id_token=${credentialResponse.credential}`
);
const userInfo = await response.json();
const result = await authenticateSSOLogin(userInfo.email);
if (result.success) {
toast.success("Logged in successfully", {
position: "bottom-center",
});
if (typeof window !== "undefined") {
window.location.reload();
}
} else {
if (result.error === "User not found") {
toast.error(result.error);
router.replace(
`/login/account_not_found?action=login&token=${credentialResponse.credential}`
);
}
toast.error(result.error);
return false;
}
} catch (error) {
console.error("Login Failed:", error);
}
},
onError: () => {
console.error("Login Failed");
if (typeof window !== "undefined") {
document.cookie =
"g_state=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
},
disabled: session,
promptMomentNotification: (notification) => {
console.log("Prompt moment notification:", notification);
},
auto_select: true,
use_fedcm_for_prompt: true,
});
return null;
}
export const GoogleSignupButton = ({ router }: Pick<Props, "router">) => {
const handleGoogleSignup = useGoogleLogin({
flow: "implicit",
onSuccess: (tokenResponse) => {
router.replace(
`/login/account_not_found?action=signup&token=${tokenResponse.access_token}`
);
},
onError: (error) => {
console.error("Login Failed:", error);
toast.error(error.error_description || "Something went wrong");
},
});
return (
<button
className= "rounded-md text-base font-medium border hover:bg-gray-200 hover:text-black h-10 px-4 py-2 w-full flex justify-center gap-4 items-center space-x-2"
type = "button"
onClick = {() => handleGoogleSignup()}>
<GoogleIcon />
< span > Sign up with Google < /span>
< /button>
);
};
Adding these buttons to the login and register page should handle the logic for logging in users. Notice I am receiving user information and calling another function, authenticateSSOLogin
since I need to check whether the user exists in my database or else register them.
Here is what the function looks like:
"use server";
import { prisma } from "@/db/prisma";
import * as jose from "jose";
import { cookies } from "next/headers";
import { registerUsers } from "./register";
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
export async function authenticateSSOLogin(email: string) {
const cookieStore = await cookies();
try {
const user = await prisma.user.findUnique({
where: { email: email },
});
if (!user) {
return { success: false, error: "User not found" };
}
const token = await new jose.SignJWT({
userId: user.id,
email: user.email,
role: user.role,
})
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("8h")
.sign(JWT_SECRET);
cookieStore.set("token", token, {
httpOnly: true,
maxAge: 8 * 60 * 60,
sameSite: "strict",
});
return { success: true, message: "User updated successfully" };
} catch (error) {
console.error(error);
return { success: false, error: "Something went wrong" };
}
}
interface Data {
username: string;
email: string;
password: string;
phone: string;
role?: string;
imageUrl?: string;
metadata?: {};
}
export async function registerSSOUsers(data: Data) {
const cookieStore = await cookies();
try {
const user = await registerUsers(data);
const token = await new jose.SignJWT({
userId: user.id,
email: user.email,
role: user.role,
})
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("8h")
.sign(JWT_SECRET);
cookieStore.set("token", token, {
httpOnly: true,
maxAge: 8 * 60 * 60,
sameSite: "strict",
});
return { success: true, message: "User registered successfully" };
} catch (error: any) {
console.error("Error in while registering users:", error);
return {
success: false,
error: error.message || "something went wrong",
};
}
}
We can also use the defined GoogleOneTapLogin
To log in to our users automatically by adding it to the root layout and passing the user object so it pops up whenever the user is not logged in.
import { GoogleContextProvider } from "@/providers/google";
import { getUserData } from "@/lib/actions/decodetoken";
import { GoogleOneTapLogin } from "../(auth)/login/google";
interface user {
id: number;
username: string;
email: string;
phone: number | null;
role: string;
image: string | null;
}
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const User = (await getUserData()) as user | null;
return (
<html lang="en">
<body>
<GoogleContextProvider>
<GoogleOneTapLogin session={User} />
<main> {children}</main>
</GoogleContextProvider>
</body>
</html>
);
}
With this setup, we are ready to take advantage of all auth features provided by Google andsafelyy store the user session in a cookie and in the database to ensure they can opt to log in with email and password or their Google account associated with their registered email.
Conclusion
By following these steps, you've successfully integrated Google SSO login into your Next.js application. This provides a seamless and convenient authentication experience for your users. Remember to handle user data securely and comply with relevant privacy regulations.
This is a basic implementation, and you can customize it further by adding features like user profile management, email verification, and more. You can simplify this process by using Clerk or Kinde for user authentication.
This thread is open to discussion
✨ Be the first to comment ✨