Sign in / Sign up with Google OAuth 2.0 failed

23 hours ago 1
ARTICLE AD BOX

I'm using Google authentication on my website built with Next+TypeScript Prisma and SupaBase, but somehow at some point I stopped being able to authenticate with Google. I've even changed my Google Client ID and all other credentials related to the Google OAuth API, and it still doesn't work. Does anyone know why this is happening?

enter image description here

Console output:

GET /api/auth/callback/google?state=6erywTJYITsJ98B5yeO8d8jCKNvm5Eo1HIHmmtGKNd4&code=4%2F0AfrIepDCcfh1KrHfE3xeZil1VIWMfU4d1-jwJzjVLZurwnjHeGNhCG75znSfUmDjWd9bqw&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid&authuser=1&hd=isptec.co.ao&prompt=consent 302 in 4.3s (compile: 64ms, proxy.ts: 20ms, render: 4.3s) [next-auth][error][OAUTH_CALLBACK_ERROR] https://next-auth.js.org/errors#oauth_callback_error invalid_grant (Bad Request) { error: Error [OAuthCallbackError]: invalid_grant (Bad Request) at ignore-listed frames { code: undefined }, providerId: 'google', message: 'invalid_grant (Bad Request)' } GET /api/auth/callback/google?state=6erywTJYITsJ98B5yeO8d8jCKNvm5Eo1HIHmmtGKNd4&code=4%2F0AfrIepDCcfh1KrHfE3xeZil1VIWMfU4d1-jwJzjVLZurwnjHeGNhCG75znSfUmDjWd9bqw&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid&authuser=1&hd=isptec.co.ao&prompt=consent 302 in 1060ms (compile: 45ms, proxy.ts: 15ms, render: 999ms) GET /api/auth/error?error=OAuthCallback 302 in 44ms (compile: 17ms, proxy.ts: 13ms, render: 14ms) GET /api/auth/signin?error=OAuthCallback 302 in 53ms (compile: 15ms, proxy.ts: 12ms, render: 26ms) GET /login?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F&error=OAuthCallback 200 in 280ms (compile: 11ms, proxy.ts: 10ms, render: 259ms) GET /api/auth/session 200 in 68ms (compile: 16ms, proxy.ts: 14ms, render: 39ms)

This is my auth.ts:

import type { NextAuthOptions } from "next-auth"; import type { JWT } from "next-auth/jwt"; import GoogleProvider from "next-auth/providers/google"; import CredentialsProvider from "next-auth/providers/credentials"; import { z } from "zod"; import bcrypt from "bcryptjs"; import { prisma } from "@/lib/prisma"; import { UserStatus, UserType } from "@prisma/client"; function requireEnv(name: string): string { const value = process.env[name]; if (!value) { throw new Error(`Missing required env var: ${name}`); } return value; } const credentialsSchema = z.object({ email: z.string().email(), password: z.string().min(1), }); export const authOptions: NextAuthOptions = { session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60, // 30 dias }, providers: [ ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET ? [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, authorization: { params: { prompt: "consent", access_type: "offline", response_type: "code", hd: "isptec.co.ao", }, }, }), ] : []), CredentialsProvider({ name: "Credenciais", credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { const parsed = credentialsSchema.safeParse(credentials); if (!parsed.success) { throw new Error("InvalidCredentials"); } const user = await prisma.user.findUnique({ where: { email: parsed.data.email }, select: { id: true, email: true, name: true, password: true, type: true, status: true, isBlocked: true, deletionScheduledAt: true, }, }); if (!user) { throw new Error("InvalidCredentials"); } const ok = await bcrypt.compare(parsed.data.password, user.password); if (!ok) { throw new Error("InvalidCredentials"); } if (user.isBlocked) { throw new Error("AccountBlocked"); } // Permitir INACTIVE se tiver eliminação agendada (para cancelar) if (user.status !== UserStatus.ACTIVE) { if (user.status === UserStatus.INACTIVE && user.deletionScheduledAt) { // Permitir login para cancelar eliminação } else { throw new Error("AccountInactive"); } } await prisma.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() }, }); return { id: user.id, email: user.email, name: user.name, type: user.type, }; }, }), ], callbacks: { async signIn({ user, account, profile }) { if (account?.provider !== "google") return true; const email = user.email; if (!email) return false; if (!email.endsWith("@isptec.co.ao")) { console.error( "Tentativa de login com email não institucional:", email, ); return false; // Bloquear login } // Capturar foto do perfil do Google const googleProfileImage = user.image ?? (profile as { picture?: string })?.picture ?? null; const existing = await prisma.user.findUnique({ where: { email }, select: { id: true, status: true, isBlocked: true, deletionScheduledAt: true, profileImageUrl: true, }, }); if (!existing) { const randomPassword = requireEnv("NEXTAUTH_SECRET").slice(0, 16) + Date.now().toString(16); const hashedPassword = await bcrypt.hash(randomPassword, 10); const name = user.name ?? (typeof profile?.name === "string" ? profile.name : null) ?? email.split("@")[0]; // Determinar tipo de usuário baseado no email // Apenas STUDENT e TEACHER precisam de validação const userType = UserType.STUDENT; // Por padrão, usuários são estudantes const needsValidation = userType === UserType.STUDENT || userType === UserType.TEACHER; await prisma.user.create({ data: { email, name, password: hashedPassword, type: userType, status: needsValidation ? UserStatus.PENDING : UserStatus.ACTIVE, activationStatus: needsValidation ? "PENDING_DOCUMENTS" : "ACTIVE", profileImageUrl: googleProfileImage, lastLoginAt: new Date(), }, }); } else { // Bloquear se bloqueado if (existing.isBlocked) { console.error("Tentativa de login com conta bloqueada:", email); return false; } // Permitir INACTIVE com eliminação agendada (para cancelar) if ( existing.status === UserStatus.INACTIVE && !existing.deletionScheduledAt ) { console.error("Tentativa de login com conta inativa:", email); return false; } // Atualizar foto do Google se não tiver foto ou se for diferente const updateData: { lastLoginAt: Date; profileImageUrl?: string | null } = { lastLoginAt: new Date(), }; if (googleProfileImage && !existing.profileImageUrl) { updateData.profileImageUrl = googleProfileImage; console.log(`Foto do Google adicionada para: ${email}`); } await prisma.user.update({ where: { email }, data: updateData, }); } return true; }, async jwt({ token, trigger }) { if (!token.email) return token; // Apenas buscar dados do DB quando necessário (não em toda requisição) if (trigger === "signIn" || trigger === "update" || !token.id) { const dbUser = await prisma.user.findUnique({ where: { email: token.email }, select: { id: true, type: true, status: true, isBlocked: true, name: true, activationStatus: true, deletionScheduledAt: true, profileImageUrl: true, }, }); if (!dbUser || dbUser.isBlocked) { return {}; } if ( dbUser.status === UserStatus.INACTIVE && !dbUser.deletionScheduledAt ) { return {}; } token.id = dbUser.id; token.type = dbUser.type; token.name = dbUser.name; token.activationStatus = dbUser.activationStatus; token.deletionPending = !!dbUser.deletionScheduledAt; token.profileImageUrl = dbUser.profileImageUrl; } return token; }, async session({ session, token }) { if (session.user) { const typedToken = token as JWT; session.user.id = typedToken.id; session.user.type = typedToken.type; session.user.name = typedToken.name; session.user.activationStatus = typedToken.activationStatus; session.user.deletionPending = typedToken.deletionPending; session.user.profileImageUrl = typedToken.profileImageUrl; } return session; }, }, pages: { signIn: "/login", error: "/auth-error", }, secret: process.env.NEXTAUTH_SECRET, };
Read Entire Article