Recently, I integrated Google OAuth login functionality into a project and encountered numerous pitfalls from configuration to deployment. I’ve organized my experience into this article, hoping it will help other developers facing similar challenges.
Requirement: Add “One-Click Google Login” to a web app. After users log in, their accounts should be automatically created, and they should receive bonus credits.
📋 Complete Configuration Process
Step 1: Detailed Configuration in Google Cloud Console
This is the first and most error-prone step—many developers stumble here!
1. Create a Google Cloud Project
Step 1: Access the Google Cloud ConsoleOpen the Google Cloud Console, then log in with your Google account.
Step 2: Create a new project
Click the project selector at the top of the page.
Click the “New Project” button.
Enter a project name (e.g., “my-nextjs-app”).
Select an organization (individual developers can choose “No Organization”).
Click “Create”.
💡 Tip: Use an English project name to avoid encoding issues in subsequent configurations.
2. Enable Google+ API (Critical!)
Many developers skip this step, leading to authentication failures later:
Step 1: Navigate to the API Library
In the left sidebar, go to “APIs & Services” → “Library”.
Alternatively, search directly for “Google+ API”.
Step 2: Enable Required APIsYou need to enable the following APIs:
✅ Google+ API (deprecated, but still required for certain features)
✅ People API (recommended for retrieving user information)
✅ Gmail API (only if you need access to email data)
Step 3: Enable the APIs
Click on each API card.
Click the “Enable” button.
Wait for the enablement to complete (usually takes a few seconds).
⚠️ Note: If you don’t enable these APIs, you’ll encounter an “access_denied” error later!
3. Configure the OAuth Consent Screen (Key Step)
This step determines the login authorization page users see:
Step 1: Access OAuth Consent Screen settingsIn the left sidebar, go to “APIs & Services” → “OAuth Consent Screen”.
Step 2: Select User Type
🔸 Internal: For users within your organization (requires Google Workspace).
🔸 External: For any Google user (recommended).Choose “External” if you’re an individual developer or building a public app.
Step 3: Fill in App InformationBasic Information:
App Name: The name users see on the authorization page.
User Support Email: Your contact email address.
App Logo (optional): Recommended to upload a 120x120px PNG.
App Homepage: Your app’s homepage URL.
App Privacy Policy: Link to your privacy policy page.
App Terms of Service (optional): Link to your terms of service page.
Missing test users (if the app is in Testing status).
Solutions:
Ensure the People API is enabled.
Fill in all required fields on the OAuth Consent Screen.
Add test user emails.
Error 3: invalid_client
Error Message: Error: invalid_client
Cause: Incorrect Client ID or Client Secret.
Solutions:
Verify the GOOGLE_CLIENT_ID in your environment variables.
Confirm the Client Secret was copied correctly.
Check for hidden characters or extra spaces in the values.
🔥 Pitfalls I Encountered
Pitfall 1: Server Error – Server Configuration Issue
Symptom: After clicking “Sign in with Google”, the page shows: “Server error – There is a problem with the server configuration”.
Root Cause: Incomplete or incorrect environment variable configuration.
Solution:Ensure your .env.local file includes the following:env# .env.local (required configuration) NEXTAUTH_SECRET="your_generated_secret" NEXTAUTH_URL="https://your-domain.com" GOOGLE_CLIENT_ID="your_google_client_id" GOOGLE_CLIENT_SECRET="your_google_client_secret"
Key Reminders:
Generate NEXTAUTH_SECRET using the command: openssl rand -base64 32.
Reconfigure environment variables in Vercel after deployment.
Ensure NEXTAUTH_URL matches the correct domain for local/production environments.
Pitfall 2: “This action with HTTP GET is not supported”
Symptom: The URL redirects to /api/auth/error with a “GET method not supported” message.
Key Discovery: NextAuth.js uses a fixed callback URL format: /api/auth/callback/[provider]
📋 NextAuth.js Code Configuration
Below is the complete NextAuth.js configuration file for App Router (Next.js 13+). Save it at: app/api/auth/[...nextauth]/route.ts
typescript
import {Provider} from "next-auth/providers/index";
import GoogleProvider from "next-auth/providers/google";
import {AuthOptions} from "next-auth";
import NextAuth from "next-auth";
import {genUniSeq, getIsoTimestr} from "@/backend/utils";
import {saveUser} from "@/backend/service/user";
import {User} from "@/backend/type/type";
import {createCreditUsage} from "@/backend/service/credit_usage";
import {getCreditUsageByUserId} from "@/backend/service/credit_usage";
/**
* Initialize the array of authentication providers
* Stores all available login methods (Google, GitHub, Facebook, etc.)
*/
let providers: Provider[] = [];
/**
* Configure Google OAuth provider
* Load Google app client configuration from environment variables
*/
providers.push(
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "", // Google app client ID
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", // Google app client secret
// 🚨 Important: Do NOT manually set redirect_uri—NextAuth.js handles this automatically
})
);
/**
* NextAuth.js configuration options
* Contains callbacks and parameters for the authentication flow
*/
const authOptions: AuthOptions = {
secret: process.env.NEXTAUTH_SECRET, // Secret for encrypting JWT tokens
providers, // List of authentication providers
callbacks: {
/**
* Sign-in verification callback
* Called when a user attempts to log in; determines if login is allowed
*/
async signIn({user, account, profile, email, credentials}) {// Add login restriction logic here (e.g., whitelist checks, ban status)
const isAllowedToSignIn = true;
if (isAllowedToSignIn) {return true;} else {
// Return false to deny login, or a URL to redirect to an error page
return false;
}
},
/**
* Redirect callback
* Controls page navigation after the authentication flow completes
*/
async redirect({url, baseUrl}) {
// Redirect to homepage after successful login
// Customize to redirect to role-specific pages if needed
return `${baseUrl}/`;
},
/**
* Session callback
* Called each time a session is fetched; customizes the session object sent to the client
*/
async session({session, token, user}) {
// Merge user data from JWT token into the session
// Allows the frontend to access full user info via useSession()
if (token && token.user) {session.user = token.user;}
return session;
},
/**
* JWT callback
* Called when a JWT token is created or updated
* Core function for user registration and database operations
*/
async jwt({token, user, account}) {// Only run on initial login (user/account params exist only on first login)
if (user && user.email && account) {
try {
// Create a database user object
const dbUser: User = {uuid: genUniSeq(), // Generate unique user ID
email: user.email, // Google account email
nickname: user.name || "", // Google account display name
avatar_url: user.image || "", // Google account avatar URL
signin_type: account.type, // Login type (oauth)
signin_provider: account.provider, // Login provider (google)
signin_openid: account.providerAccountId, // Unique Google user ID
created_at: getIsoTimestr(), // Creation time (ISO format)
signin_ip: "", // Login IP (leave empty; add IP-fetching logic later if needed)
};
// Save user to database
// The saveUser function typically handles updates if the user already exists
await saveUser(dbUser);
// Check if the user already has a credit record
const creditUsage = await getCreditUsageByUserId(dbUser.uuid);
// If new user: create a credit record and grant initial bonus credits
if (!creditUsage) {
await createCreditUsage({
user_id: dbUser.uuid, // User ID
user_subscriptions_id: -1, // Subscription ID (-1 = no subscription)
is_subscription_active: false, // Subscription status (inactive)
used_count: 0, // Number of credits used
period_remain_count: 20, // Remaining credits (20 for new users)
period_start: new Date(), // Credit period start date
period_end: new Date(// Credit period end date (1 month later)
new Date().setMonth(new Date().getMonth() + 1)
),
created_at: new Date(), // Record creation time});
}
// Store user data in JWT token
// This data will be used in the session callback later
token.user = {
uuid: dbUser.uuid,
nickname: dbUser.nickname,
email: dbUser.email,
avatar_url: dbUser.avatar_url,
created_at: dbUser.created_at,
};
} catch (error) {console.error("Error processing user data in JWT callback:", error);
// Continue authentication even if database operations fail
// Avoid disrupting the user login experience
}
}
return token;
},
},
};
/**
* Create NextAuth handler
* Initialize NextAuth with the configuration options above
*/
const handler = NextAuth(authOptions);
/**
* Export the handler
* For App Router (Next.js 13+), export both GET and POST methods
* Handles all HTTP requests for NextAuth.js
*/
export {handler as GET, handler as POST};
TikTok is giving users new ways to interact with others via direct messages (DMs), the company told TechCrunch on Friday. Users will now be able to send voice notes and share up to nine images or videos in one-to-one and group chats on the platform. With these new features, TikTok is positioning itself as more...
Steven Kleinveld, founder of Skylark, explains why developers need to embrace AI Founders’takes is a new series featuring expert insights from tech leaders transforming industries with artificial intelligence. In this edition, Steven Kleinveld, founder of applied AI lab Skylark, argues that vibe coding won’t replace developers — it’ll upgrade them. There’s been a lot of talk lately that...
This article details the “interrupt service routine variable not updating” issue in embedded ARM development, which stems from compiler optimization and missing volatile modifiers. It uses examples to explain errors caused by compiler “redundant load elimination”, illustrates how the volatile keyword prohibits register caching and maintains instruction order, and compares optimization differences at the ARM...