Integrating Google, Strapi NextJS with Next-Auth Login
Tested and worked in Strapi 5
For this I did not use any plugins or backend customization, except the mandatory configuration in admin page as in Strapi DOCs - User Permissions: https://docs.strapi.io/dev-docs/plugins/users-permissions#jwt-configuration (opens in a new tab)
Must create credentials in your chosen provider, for ex google: Google API Credentials: https://console.cloud.google.com/apis/credentials (opens in a new tab)
Provider configuration
./lib/auth.ts
import type { NextAuthOptions, DefaultSession } from 'next-auth';
import CredentialsProvider from "next-auth/providers/credentials"; // Local provider
import GoogleProvider from "next-auth/providers/google";
// import Facebook from "next-auth/providers/facebook";
declare module "next-auth" {
interface Session {
user: {
jwt: string;
} & DefaultSession["user"]
}
interface User {
password?: string;
}
}
const api_url = process.env.NEXT_PUBLIC_API_URL;
const token = process.env.STRAPI_TOKEN; // you configure @ {STRAPI_URL}/admin/settings/api-tokens, check: https://docs.strapi.io/user-docs/settings/API-tokens
export const authConfig: NextAuthOptions = {
secret: process.env.NEXT_AUTH_SECRET, // Generate in git terminal: 'openssl rand -base64 32' or copy from https://generate-secret.vercel.app/32
providers: [
GoogleProvider({
// Must configure both at: https://console.cloud.google.com/apis/credentials
clientId: process.env.GOOGLE_CLIENT_ID as string, // Calling .env file in frontend (You must insert same in Strapi Provider Page)
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, // Calling .env file in frontend (You must insert same in Strapi Provider Page)
}),
// ENABLED WHEN FACEBOK APROVE
// Facebook({
// clientId: process.env.AUTH_FACEBOOK_ID as string,
// clientSecret: process.env.AUTH_FACEBOOK_SECRET as string,
// }),
CredentialsProvider({
// The name to display on the sign in form (e.g. "Sign in with...")
name: "Email",
// `credentials` is used to generate a form on the sign in page.
// You can specify which fields should be submitted, by adding keys to the `credentials` object.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
email: { label: "Email", type: "text", placeholder: "Email" },
password: { label: "Senha", type: "password" }
},
async authorize(credentials, req) { // Ask authorization from some provider
if (credentials) { // Credentials from provider
try {
/* ONLY LOCAL SERVER LOGIN */
const url = `${api_url}/api/auth/local`;
const user = {
identifier: email, // On Post request use 'indentifier' property, strapi will respond with 'email' property
password: credentials.password
}
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}` // You must create strapi token
},
body: JSON.stringify(user)
});
const data = await response.json();
if ("user" in data) {
const { jwt, user } = data;
if (!user.email || !user.id) {
console.error("Invalid user data:", user.email);
return null;
}
return {
jwt, // Modifying object to put JWT in first level
id: `${user.id}`,
email: user.email,
password: credentials.password
};
}
console.error("Invalid response in authorize ", response);
return null; // Login Error
} catch (error) {
console.log("Credentials authorize error", error);
return null; // Network Error
}
}
return null; // If here all have failed
}
}),
],
callbacks: { // Function to run after authorization
async signIn({ user, account }) {
try {
const { email } = user;
// Basic check to passs
if (!email) {
console.error("Incomplete data: Email or Password missing.");
return false;
}
if (account?.provider && user.email) {
return true;
}
// Case using local Provider, go straight to sign in
if(!("password" in user)){
return false;
} else {
/* Local Server Login */
const password = user.password as string; // Password from default next-auth button or custom login page
const user = {
identifier: email,
password
}
const url = `${api_url}/api/auth/local`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if("user" in data && data?.user){
return true;
}
}
return false;
} catch (error) {
console.error("SignIn Error:", error instanceof Error ? error.message : error);
return false;
}
},
async jwt({ token, user, account }) { // Function when it has JWT
/* Provider Time: Google|Facebook|Github */
if (account?.access_token) {
const url = `${api_url}/api/auth/${account.access_token}/callback?access_token=${account?.access_token}`; // Confirm if frontend credentials match backend
/* This URL is Always GET method, but it can LOGIN or CREATE the user in database returning data + jwt */
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if ("jwt" in data) {
account.access_token = data.jwt; // Reaplace to Strapi token
}
}
return {
// My preference adjustment as I prefer in my application
...token,
...user,
...account,
jwt: account?.access_token || token.jwt, // account?.access_token = External Server jwt
};
},
async session({ session, token, }) {
const { password, ...tokenNoPassowrd } = token; // Removing password
return {
// Customizing object again as I need
...session,
user: {
jwt: token.access_token,
...tokenNoPassowrd,
},
}
},
},
pages: {
// Only if want to replace default next-auth Login Page
// Can use default App structure: ./src/app/account/page.tsx I custom to ./src/app/(pages)/account/page.tsx
signIn: "/account/login",
},
} satisfies NextAuthOptions;
One more file to replace the route.
.src/app/api/[...nextauth]/route.ts
import { authConfig } from "@/app/lib/auth"; // Importing above file auth.ts
import NextAuth from "next-auth/next";
const handler = NextAuth(authConfig);
export { handler as GET, handler as POST }; // I did not need to adjust for PUT method for example
Provide to all routes
./src/app/layout.tsx
import './globals.css';
import Header from "./components/Header"
import Footer from "./components/Footer"
import { NextAuthProvider } from "./providers/nextAuthProvider" // Important
export default async function RootLayout({children}: {children: React.ReactNode}) {
return (
<html lang="pt">
<NextAuthProvider> {/*Important*/}
<head></head>
<body>
<Header />
<main className="flex min-h-screen flex-col my-4">
{children}
</main>
<Footer />
</body>
</NextAuthProvider>
</html>
)
}
./src/app/user/page.tsx
"use client" // Mandatory due to useSession, to convert to server remove this line and use getServerSession, see: https://next-auth.js.org/configuration/nextjs
import React, { useEffect, useState } from 'react'
import { useSession } from "next-auth/react"; // Important
export default function UserPage() {
const session = useSession(); // useSession hook as provided by NextAuthProvider
const user = session?.data?.user; // My custom object as modified in auth.ts
const jwt = user?.jwt; // My custom object as modified in auth.ts
/*
Eg:
useEffect hook to fetch favourite items in strapi passing jwt and setting the state from response
*/
return (
<div>
user name: {user?.name}
user email: {user?.email}
</div>
);
}
Last updated on