Complete Guide to OAuth Login in Next.js: Configuring Google, GitHub, and WeChat Authentication

Click “Sign in with Google,” the page redirects to Google, you authorize, it redirects back, and boom—you’re logged in.
You’ve experienced this countless times. But when you try to add third-party login to your own project, suddenly you’re lost: Why two redirects? What’s a callback URL? What the heck is redirect_uri_mismatch? Works locally but breaks in production?
To be honest, I felt the same way when I first configured OAuth. Stared at documentation full of terms like “authorization code,” “access_token,” “client_secret”—got more confused the more I read. Took me two days and countless mistakes to barely figure it out.
In this article, I want to explain OAuth login in the simplest way possible. No jargon overload, no abstract theory—just how it actually works and how to configure Google, GitHub, and WeChat login in Next.js. You’ll realize it’s not that complicated after all.
What Is OAuth 2.0? (In Plain English)
Start with a Real-Life Example
Imagine you live in a gated community. One day you order something online and want the delivery person to bring it to your door. Problem: there’s a security gate, and the delivery person can’t get in.
Traditional approach? Give the delivery person your access card. But that’s risky—what if they use your card to enter anytime they want later?
Smarter approach: You go to the security guard and say “I have a delivery coming.” The guard gives the delivery person a temporary pass that says “Valid only 2-4pm today, access to Building A only.” After the delivery, the pass expires.
That’s the core idea behind OAuth.
In this analogy:
- You = The user (the person logging in)
- Delivery person = Third-party app (your website)
- Security guard = OAuth provider (Google, WeChat, GitHub, etc.)
- Access card = Your password (can’t share it)
- Temporary pass = access_token (time-limited, permission-restricted)
You don’t give your password to the third-party app—you just authorize it to get a “temporary pass” from the OAuth provider.
The 5-Step OAuth Flow (Authorization Code Grant)
Now let’s see how this applies to Next.js OAuth login.
Step 1: User clicks “Sign in with Google” on your website.
Step 2: Your website redirects the user to Google’s authorization page with a URL like this:
https://accounts.google.com/o/oauth2/auth?
client_id=YourAppID
&redirect_uri=http://localhost:3000/api/auth/callback/google
&response_type=code
&scope=openid email profile
&state=RandomStringAt this step, your website is telling Google: “Hey, I’m App XYZ (client_id), the user wants to log in using your account. Please ask them to confirm. When done, send them back to this address (redirect_uri).”
Step 3: User sees “App XYZ wants to access your basic info” on Google’s page and clicks “Allow.”
Step 4: Google redirects the user back to your website (redirect_uri) with an authorization code in the URL:
http://localhost:3000/api/auth/callback/google?code=ABCD1234&state=RandomStringNote: This code is just a voucher, not the final pass. It has a short expiration (usually 10 minutes) and can only be used once.
Step 5: Your website’s backend takes this code, along with your client_secret (password), and exchanges it for the real access_token from Google:
// Backend code (simplified)
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: JSON.stringify({
code: 'ABCD1234',
client_id: 'YourAppID',
client_secret: 'YourAppPassword',
redirect_uri: 'http://localhost:3000/api/auth/callback/google',
grant_type: 'authorization_code',
}),
})
const { access_token } = await response.json()With the access_token, your website can now fetch user info (email, avatar, name, etc.) from Google.
Step 6 (optional): Use access_token to get user info:
const userInfo = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
Authorization: `Bearer ${access_token}`,
},
})Once this flow completes, your website knows “who this user is” and can create a session to keep them logged in.
Key Concepts Explained (In Human Terms)
Still a bit confused about the terminology? Let me break it down:
client_id: Your app’s “ID number” at Google. It’s public—no harm if others see it.
client_secret: Your app’s “password.” Must never be leaked—only use it on the backend. If someone gets your client_secret, they can impersonate your app and access user data.
redirect_uri: Where Google sends the user after authorization. This URL must be pre-registered in Google Cloud Console. Google strictly validates it—even an extra slash will cause a redirect_uri_mismatch error (yeah, that frustrating one).
state: A random string to prevent CSRF attacks. You generate this state when initiating authorization, and Google returns it unchanged. You verify the returned state matches what you sent—if not, the request might be forged.
code: Temporary authorization code. Short-lived (around 10 minutes), single-use. Its purpose is to prove “the user has granted authorization at Google.”
access_token: The actual “pass.” With it, your website can access user info on behalf of the user. Access tokens also have expiration, usually 1 hour to a few days.
Why Split Into code and access_token?
You might wonder: Why not return access_token directly instead of using a code first?
It’s about security. The code is passed through browser redirects (visible to the frontend), while access_token is exchanged server-to-server (hidden from frontend). If access_token were directly in the URL, browser history, logs, and network monitoring could leak it. Exchanging code for token requires client_secret, which only exists on the backend—much more secure.
Setting Up Google Login with Next.js + NextAuth.js (Easiest Start)
Why Choose NextAuth.js?
Implementing OAuth manually is tedious: handling callbacks, managing sessions, preventing CSRF attacks, storing tokens… lots of details.
Good news: there’s a library called NextAuth.js (now Auth.js v5) that does exactly this. It has 15k+ stars on GitHub, active community, supports 50+ OAuth providers (Google, GitHub, WeChat, Twitter, etc.) out of the box. The latest version supports Next.js 14+ App Router and is easier to configure than before.
Simply put, NextAuth.js saves you 80% of the repetitive work.
Step 1: Create App in Google Cloud
Before writing code, you need to “register” your app with Google to get client_id and client_secret.
Open Google Cloud Console and log in with your Google account.
If you haven’t used it before, create a Project first. Name it anything, like “My Next.js App.”
In the left menu, go to “APIs & Services” → “Credentials.”
Click “Create Credentials” → select “OAuth client ID.”
If it’s your first time, you’ll be asked to configure the “OAuth consent screen.” Fill in app name and support email, skip the rest for now (choose “External” user type, no review needed for testing).
Back to creating OAuth client ID, choose application type “Web application.”
Here’s the key part: Configure “Authorized redirect URIs.” Add two addresses:
- Local development:
http://localhost:3000/api/auth/callback/google - Production (add after deployment):
https://yourdomain.com/api/auth/callback/google
Note: This address must exactly match what’s in your code. One extra or missing slash triggers redirect_uri_mismatch. I made this mistake when starting out.
- Local development:
Click “Create,” and a dialog shows your Client ID and Client Secret. Copy these values—you’ll need them soon.
Step 2: Install NextAuth.js and Configure Environment Variables
In your Next.js project, install the package:
npm install next-auth@betaNote: Install the @beta version—this is v5 (the latest).
Then create .env.local file in project root and add the Client ID and Secret you just got:
GOOGLE_CLIENT_ID=YourClientID.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=YourClientSecret
NEXTAUTH_SECRET=GenerateARandomString
NEXTAUTH_URL=http://localhost:3000Generate NEXTAUTH_SECRET with this command:
openssl rand -base64 32When deploying to production, remember to change NEXTAUTH_URL to your domain.
Step 3: Create NextAuth Configuration File
Create file at app/api/auth/[...nextauth]/route.ts (if using Pages Router, path is pages/api/auth/[...nextauth].ts):
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
export const authOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
async signIn({ user, account, profile }) {
// Callback on successful login, save user info to database here
console.log("User logged in:", user)
return true // Return true to allow login
},
async session({ session, token }) {
// Customize session content
if (session.user) {
session.user.id = token.sub // Add user id to session
}
return session
},
},
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }That’s it—NextAuth.js automatically handles the entire OAuth flow. The /api/auth/callback/google route is auto-generated, no extra code needed.
Step 4: Create Login Button
In any component, you can write a login button like this:
'use client' // App Router requires client component notation
import { signIn, signOut, useSession } from "next-auth/react"
export default function LoginButton() {
const { data: session } = useSession()
if (session) {
// User is logged in
return (
<div>
<p>Welcome, {session.user?.name}</p>
<img src={session.user?.image || ''} alt="Avatar" />
<button onClick={() => signOut()}>Sign Out</button>
</div>
)
}
// User is not logged in
return <button onClick={() => signIn('google')}>Sign in with Google</button>
}signIn('google') automatically redirects to Google’s authorization page, and after authorization, redirects back to log the user in. Super easy.
Step 5: Wrap Root Layout with SessionProvider
To make useSession available in all components, wrap a Provider in the root layout:
// app/layout.tsx
import { SessionProvider } from "next-auth/react"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<SessionProvider>{children}</SessionProvider>
</body>
</html>
)
}Done! Now you have a Next.js app with Google login.
Troubleshooting Common Issues
Issue 1: redirect_uri_mismatch
This is the most common error—I encountered it my first time too.
Error message looks like: Error 400: redirect_uri_mismatch
Cause: The callback address configured in Google Cloud Console doesn’t match the actual request.
Solution:
- Check if “Authorized redirect URIs” in Google Cloud Console is
http://localhost:3000/api/auth/callback/google(watch for extra slashes) - Check if
NEXTAUTH_URLin your.env.localishttp://localhost:3000 - If you changed the port (e.g., 3001), update both places
Issue 2: Works Locally but Fails After Deployment
I’ve made this mistake too. Local testing works perfectly, but after deploying to Vercel, clicking login does nothing or errors out.
Cause: Forgot to update production environment variables.
Solution:
- In Google Cloud Console’s “Authorized redirect URIs,” add production domain:
https://yourdomain.com/api/auth/callback/google - In Vercel (or your hosting platform) environment variables settings, change
NEXTAUTH_URLtohttps://yourdomain.com - Redeploy
Issue 3: Session Is null After Login
If useSession() always returns null for session, check if you forgot to wrap <SessionProvider> in the root layout.
Issue 4: TypeError: Cannot read property ‘user’ of null
Usually because the session is still loading when you try to access session.user.
Solution: Check if session exists first:
const { data: session, status } = useSession()
if (status === 'loading') {
return <div>Loading...</div>
}
if (!session) {
return <div>Not logged in</div>
}
// Safe to access session.user hereConfiguring GitHub Login (Understanding the Differences)
With Google login experience, configuring GitHub login is easier. But GitHub and Google have some differences worth discussing.
Differences Between GitHub and Google OAuth
Similarities: Both use standard OAuth 2.0 authorization code flow—the process is identical.
Differences:
- Finer permission control: GitHub’s scope (permission range) is more complex. By default, you only get public info. To get user email (especially private email), you need to request
user:emailpermission separately. - More flexible callback URL configuration: Google requires the full callback URL, GitHub only needs the domain.
- Application types: GitHub supports both personal and organization OAuth Apps.
Step 1: Create OAuth App on GitHub
Log into GitHub, click profile avatar → Settings → find “Developer settings” in left menu.
Click “OAuth Apps” → “New OAuth App.”
Fill in application info:
- Application name: Your app name (users will see this during authorization)
- Homepage URL: Your website homepage, like
http://localhost:3000 - Authorization callback URL: Enter
http://localhost:3000/api/auth/callback/github
Click “Register application,” then click “Generate a new client secret,” and copy the Client ID and Client Secret.
Step 2: Configure Environment Variables
Add GitHub configuration to .env.local:
GITHUB_CLIENT_ID=YourGitHubClientID
GITHUB_CLIENT_SECRET=YourGitHubClientSecretStep 3: Update NextAuth Configuration
In the previous route.ts file, add GitHubProvider:
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import GitHubProvider from "next-auth/providers/github"
export const authOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
// To get user's private email, add this configuration
authorization: {
params: {
scope: 'read:user user:email'
}
}
}),
],
callbacks: {
// ... previous callbacks
},
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }Step 4: Update Login Button
Add GitHub login option to the login button component:
return (
<div>
<button onClick={() => signIn('google')}>Sign in with Google</button>
<button onClick={() => signIn('github')}>Sign in with GitHub</button>
</div>
)That’s it—done!
About scope (Permission Range)
GitHub’s scope controls what user info your app can access. Common ones:
read:user: Read user’s public and private info (name, avatar, bio, etc.)user:email: Read user’s email (including private email)public_repo: Access user’s public repositoriesrepo: Access all user repositories (public + private, powerful permission, use carefully)
For login scenarios, read:user user:email is sufficient.
If you don’t include user:email scope, NextAuth.js can only get the user’s public email. If the user hid their email in GitHub settings, session.user.email will be null. I was confused about this for a while, thinking I had a code bug.
A Small Gotcha: GitHub Users May Not Have Public Email
Unlike Google (which requires email), GitHub users can choose not to make their email public. If your app requires user email (e.g., for notifications), check in the signIn callback:
async signIn({ user, account }) {
if (account?.provider === 'github' && !user.email) {
// User didn't provide email, can reject login or prompt user
console.log("GitHub user didn't provide email")
return false // Reject login
}
return true
}Configuring WeChat Login (Special Case in China)
Now let’s talk about WeChat login. To be honest, WeChat login is more complex than Google and GitHub, mainly because its ecosystem is quite different from international platforms.
Why Is WeChat Login Special?
First, key differences:
- Requires QR code scanning: For PC website WeChat login, users need to take out their phone and scan a QR code to authorize (unlike Google/GitHub where you just click on the webpage).
- No official NextAuth.js Provider: NextAuth.js doesn’t have built-in WeChat provider—you have to write a custom one.
- More callback URL restrictions: WeChat requires callback domains to be registered with authorities, can’t use localhost, making local development tricky.
- Has openid and unionid: WeChat’s user identifier is special—same user has different openid in different apps. If you have multiple apps and want to share user data, you need unionid.
Step 1: Register on WeChat Open Platform
Open WeChat Open Platform and register an account.
Create a “Website Application” (not Official Account or Mini Program).
Fill in website info, upload screenshots, wait for review. Review usually takes 1-3 business days.
After approval, you’ll get AppID and AppSecret (equivalent to client_id and client_secret).
In “Development Info,” configure authorization callback domain (only enter domain name, no full path needed, e.g.,
yourdomain.com).
Step 2: Custom WeChat Provider
Since NextAuth.js doesn’t have built-in WeChat provider, we need to write our own. Create lib/wechat-provider.ts in your project:
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers"
export interface WeChatProfile {
openid: string
nickname: string
headimgurl: string
sex: number
province: string
city: string
country: string
unionid?: string
}
export default function WeChatProvider<P extends WeChatProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "wechat",
name: "WeChat",
type: "oauth",
// WeChat authorization URL (for PC website apps)
authorization: {
url: "https://open.weixin.qq.com/connect/qrconnect",
params: {
scope: "snsapi_login",
appid: options.clientId,
response_type: "code",
},
},
// URL to exchange authorization code for access_token
token: {
url: "https://api.weixin.qq.com/sns/oauth2/access_token",
params: {
appid: options.clientId,
secret: options.clientSecret,
grant_type: "authorization_code",
},
},
// URL to get user info
userinfo: {
url: "https://api.weixin.qq.com/sns/userinfo",
async request({ tokens, provider }) {
const res = await fetch(
`${provider.userinfo?.url}?access_token=${tokens.access_token}&openid=${tokens.openid}&lang=zh_CN`
)
return await res.json()
},
},
// Convert WeChat user info to NextAuth standard format
profile(profile) {
return {
id: profile.openid,
name: profile.nickname,
email: null, // WeChat doesn't provide email
image: profile.headimgurl,
}
},
options,
}
}Step 3: Configure Environment Variables
Add WeChat configuration to .env.local:
WECHAT_CLIENT_ID=YourWeChatAppID
WECHAT_CLIENT_SECRET=YourWeChatAppSecretStep 4: Use in NextAuth
Update route.ts:
import WeChatProvider from "@/lib/wechat-provider"
export const authOptions = {
providers: [
GoogleProvider({...}),
GitHubProvider({...}),
WeChatProvider({
clientId: process.env.WECHAT_CLIENT_ID!,
clientSecret: process.env.WECHAT_CLIENT_SECRET!,
}),
],
}Step 5: How to Test Locally?
This is a pain point. Since WeChat doesn’t allow localhost as callback domain, you can’t test directly in local development.
Two solutions:
Solution 1: Use Tunneling Tool
Use ngrok or cpolar to expose your local service to the public internet:
# Install ngrok
brew install ngrok
# Start tunnel
ngrok http 3000ngrok gives you a temporary domain like https://abc123.ngrok.io. Add this domain to WeChat Open Platform’s authorization callback domain, then update NEXTAUTH_URL in .env.local:
NEXTAUTH_URL=https://abc123.ngrok.ioSolution 2: Configure hosts File
Add a line to local hosts file (Mac/Linux: /etc/hosts, Windows: C:\Windows\System32\drivers\etc\hosts):
127.0.0.1 dev.yourdomain.comThen access http://dev.yourdomain.com:3000 and configure callback domain in WeChat Open Platform as dev.yourdomain.com.
But there’s a problem: WeChat requires callback domains to be registered with authorities, so dev.yourdomain.com still won’t work. Solution 1 is more reliable.
Special Handling for WeChat Login
WeChat users don’t have email. If your app depends on email, handle it accordingly:
async signIn({ user, account }) {
if (account?.provider === 'wechat') {
// WeChat user has no email, can ask user to fill it in
// or use openid as unique identifier in database
console.log("WeChat user openid:", user.id)
}
return true
}Security Best Practices (Avoiding Common Pitfalls)
After configuring OAuth login, there are security details to watch out for. I’ve made these mistakes—learned the hard way.
1. client_secret Must Never Be Leaked
Wrong approach:
// ❌ Never do this!
const clientSecret = "abc123def456" // Hardcoded in codeIf you put client_secret in frontend code or commit it to Git, anyone who gets it can impersonate your app to access user data.
Right approach:
- Store in environment variables, add
.env.localto.gitignore - Only use client_secret on backend (NextAuth.js API routes are backend, so that’s fine)
- Use platform’s environment variable management for deployment (Vercel’s Settings → Environment Variables)
2. Purpose of state Parameter (Prevent CSRF Attacks)
OAuth flow has a state parameter to prevent CSRF attacks.
Attack scenario: Hacker constructs a malicious link with a forged authorization code and tricks you into clicking it. If your app doesn’t verify state, it might be fooled into using the fake code to exchange for token.
Good news: NextAuth.js automatically handles state verification—no extra code needed.
If you implement OAuth yourself (without NextAuth.js), remember to:
- Generate a random state when initiating authorization, save it to session or cookie
- Check if the returned state matches what you sent during callback
- Reject if they don’t match
3. Callback URL Whitelist
In OAuth provider (Google, GitHub, WeChat) backends, configure all possible callback addresses:
- Development:
http://localhost:3000/api/auth/callback/[provider] - Preview:
https://preview.yourdomain.com/api/auth/callback/[provider] - Production:
https://yourdomain.com/api/auth/callback/[provider]
Don’t use wildcards (like https://*.yourdomain.com)—while convenient, it’s insecure. Explicitly list all domains to prevent exploitation by attackers.
4. Token Storage Security
NextAuth.js uses JWT by default to store sessions, with tokens saved in HttpOnly Cookies. This is excellent design:
- HttpOnly: Frontend JavaScript can’t read this cookie, preventing XSS attacks from stealing tokens
- Secure (production): Only transmitted via HTTPS, preventing man-in-the-middle attacks
What you need to do:
- Don’t return access_token to frontend (NextAuth.js doesn’t return it by default, don’t manually return it either)
- If you need to persist user info, save to database in
signIncallback, only store necessary info in session (user id, email)
5. Authorization Code Expiration
OAuth authorization codes have short expiration (10 minutes) and single-use only.
This is a security design: Even if a hacker intercepts your code, by the time they react and try to use it, the code might be expired or already used by you.
If users stay on the authorization page too long (e.g., went to make coffee), the code might expire when they return. NextAuth.js automatically handles this scenario and re-initiates authorization.
6. Production Environment Checklist
Before deployment, check these configurations:
- Are all environment variables set (NEXTAUTH_URL, NEXTAUTH_SECRET, client_id and client_secret for each provider)?
- Is NEXTAUTH_URL changed to production domain (not localhost)?
- Are production callback addresses configured in OAuth provider backends?
- Is .env.local in .gitignore (ensuring secrets aren’t committed to Git)?
- Is production NEXTAUTH_SECRET randomly generated (not using development one)?
Conclusion
Let’s quickly recap everything.
The essence of OAuth is really just a “temporary pass”: You don’t give your password to third-party apps—you just authorize them to get a time-limited, permission-restricted pass (access_token) from the OAuth provider. The flow has two steps: first use authorization code to prove user consent, then exchange code + client_secret for the actual token.
For configuring third-party login in Next.js, Google is easiest—best choice for beginners; GitHub is slightly more complex—need to pay attention to scope configuration and users may not have public email; WeChat is most special—requires QR scanning, custom provider, domain registration with authorities, and local testing is tricky.
For security, remember three principles:
- Only use client_secret on backend—never leak it
- Always verify state parameter (NextAuth.js does this for you)
- Use callback URL whitelist—no wildcards
If this is your first time configuring OAuth, I suggest starting with Google login and following the code in this article step by step. The sense of achievement when it works is pretty awesome.
Don’t panic when you hit issues—90% of errors are due to incorrect redirect_uri configuration or missing environment variables. Double-check and you’ll usually solve it.
Finally, I recommend checking out NextAuth.js official docs for more advanced features (database session storage, custom login pages, JWT configuration, etc.). The OAuth 2.0 standard specification (RFC 6749) is also worth reading. Once you understand the principles, troubleshooting becomes much easier.
Give it a try—good luck with your configuration!
FAQ
How does OAuth 2.0 work?
Flow:
1. User clicks login → redirects to OAuth provider
2. User authorizes → returns authorization code
3. Exchange code + client_secret for access_token
4. Use token to get user information
How do I fix redirect_uri_mismatch error?
Solutions:
1) Ensure the callback URL in OAuth provider backend exactly matches the code (including protocol, domain, port, path)
2) Use http://localhost:3000 for local development, actual domain for production
3) Check for extra slashes or parameters
What's the difference between NextAuth.js and manual OAuth implementation?
• Pre-packaged OAuth solution
• Supports 50+ providers
• Automatically handles authorization flow, session management, CSRF protection
Manual implementation:
• Need to handle authorization code exchange, token storage, state validation yourself
• Large codebase and error-prone
Recommendation: Use NextAuth.js.
How do I get user email addresses?
• Google: Returns email by default
• GitHub: Need user:email in scope, and user must set email as public
• WeChat: Need to query via unionid
If unavailable, let users manually fill in after login.
Works locally but breaks in production?
Check:
1) Is NEXTAUTH_URL correct in production?
2) Does OAuth provider backend callback URL include production domain?
3) Are environment variables set correctly?
4) Are there firewall or proxy issues?
Can I support multiple login methods simultaneously?
Specify the provider name in the signIn() function.
Is OAuth login secure?
But note:
1) client_secret must be kept secret, only use on server side
2) Use state parameter to prevent CSRF attacks (NextAuth.js handles automatically)
3) Use whitelist for callback URLs, don't use wildcards
4) Regularly update dependencies
15 min read · Published on: Dec 19, 2025 · Modified on: Jan 22, 2026
Related Posts
Next.js E-commerce in Practice: Complete Guide to Shopping Cart and Stripe Payment Implementation

Next.js E-commerce in Practice: Complete Guide to Shopping Cart and Stripe Payment Implementation
Complete Guide to Next.js File Upload: S3/Qiniu Cloud Presigned URL Direct Upload

Complete Guide to Next.js File Upload: S3/Qiniu Cloud Presigned URL Direct Upload
Next.js Unit Testing Guide: Complete Jest + React Testing Library Setup


Comments
Sign in with GitHub to leave a comment