Implementing Google OAuth in Next.js
A step-by-step guide to implementing Google OAuth, from redirecting users to exchanging authorization codes and retrieving user information.

Implementing Google OAuth in Next.js
In the previous article, we covered the theory behind Google OAuth, the different endpoints involved, and the parameters Google expects during authentication.
If you haven't read it yet, I highly recommend starting there first:
Getting Started with Google OAuth
https://my-personal-blog-page-website.vercel.app/articles/getting-started-with-google-oauth
In this article, we'll focus on the actual implementation of the authentication flow.
The Authentication Flow
When a user clicks "Continue with Google", our server sends the browser to Google's Authorization Endpoint together with all the required credentials we discussed in the previous article.
If the user approves the request, Google redirects the browser back to the callback URL we registered in Google Cloud Console.
One major difference between Google OAuth and GitHub OAuth is that Google enforces PKCE (Proof Key for Code Exchange), while GitHub doesn't require it for confidential server applications. Google also supports parameters like prompt, which GitHub doesn't natively enforce.
Let's build everything step by step.
Step 1: Validate Your Credentials
Before redirecting users anywhere, ensure that your Google OAuth credentials actually exist.
if (!client_id || !client_secret) {
console.log("Missing or invalid client_id or client_secret")
return NextResponse.json(
{
success: false,
message:
"Bad Request: Missing or invalid client_id or client_secret. Go to Google Cloud Console to configure your credentials."
},
{
status: 400
}
)
}
There's no point attempting OAuth if your application doesn't know who it is.
Step 2: Generate the Authorization URL
Once everything is valid, construct Google's Authorization URL.
const scope = encodeURIComponent("openid email profile")
const authorizationUrl =
`https://accounts.google.com/o/oauth2/v2/auth?client_id=${client_id}&state=${state}&redirect_uri=${encodeURIComponent(
redirect_uri
)}&response_type=code&scope=${scope}&code_challenge=${codeChallenge}&code_challenge_method=${codeChallenge_method}&prompt=select_account`
return NextResponse.redirect(authorizationUrl)
Here we're sending Google several required parameters:
| Parameter | Purpose |
| ---------- | ------- |
| client_id | Identifies our application |
| redirect_uri | Where Google should redirect after authentication |
| response_type=code | Indicates we're requesting an authorization code |
| scope | Specifies the information we want access to |
| state | Protects against CSRF attacks |
| code_challenge | Part of the PKCE security flow |
| code_challenge_method | Usually S256 |
| prompt=select_account | Forces Google to let the user choose an account |
Once Google receives this request, the browser is redirected to Google's login page.
Understanding PKCE
Since Google requires PKCE, it's important to understand what's happening.
PKCE stands for Proof Key for Code Exchange.
Its primary purpose is to prevent attackers from stealing an authorization code and exchanging it for an access token.
Instead of sending a secret directly, we send a hashed version of it. Later, during token exchange, we reveal the original value so Google can verify that it matches the hash it received earlier.
Step 3: Generate the Code Verifier
First, generate a cryptographically secure random value.
const verifyBytes = crypto.randomBytes(32)
const code_verifier = verifyBytes.toString("base64url")
Let's break this down.
crypto.randomBytes(32) generates 32 random bytes.
Since:
- 1 byte = 8 bits
- 32 × 8 = 256 bits
we end up with a 256-bit random value, which is Google's recommended size.
Those bytes aren't human-readable, so we convert them into a URL-safe string using:
.toString("base64url")
This becomes our code verifier.
Think of the code verifier as a secret password that only our application knows.
Step 4: Create the Code Challenge
Next, create a SHA-256 hash from the verifier.
const challenge = crypto.createHash("sha256")
const code_challenge = challenge.update(code_verifier)
const code_challengeString = code_challenge.digest("base64url")
Here's what's happening:
crypto.createHash("sha256")
Creates an empty SHA-256 hashing object.
challenge.update(code_verifier)
Feeds the verifier into the hashing algorithm.
Finally,
.digest("base64url")
produces the final hash in a URL-safe format.
This hashed value becomes the code challenge that we send to Google.
Later, Google will hash the verifier we send during token exchange and compare it with the challenge it already stored.
If both hashes match, Google knows it's the same application making the request.
Step 5: Store the State and Code Verifier
Before redirecting the user, we store both the state and the original code_verifier inside secure cookies.
cookiesStore.set("Oauth_state", state, {
httpOnly: true,
secure: isProduction,
sameSite: "lax",
maxAge: 600,
path: "/"
})
cookiesStore.set("Oauth_verifier", codeVerifier, {
httpOnly: true,
secure: isProduction,
sameSite: "lax",
maxAge: 600,
path: "/"
})
Notice that we do not store the hashed challenge.
Instead, we store the original verifier because we'll need it later during token exchange.
The Callback
After the user approves access, Google redirects them back to our callback URL.
The URL contains two important query parameters:
codestate
The first thing we should do is compare the returned state with the one we stored in the cookie.
If they don't match, reject the request immediately because it could be a CSRF attack.
Step 6: Exchange the Authorization Code
If everything checks out, we can exchange the authorization code for an access token.
We make a POST request to Google's Token Endpoint using the parameters specified in Google's documentation.
One important thing to know is that Google's Token Endpoint expects data in the format browsers normally submit HTML forms:
application/x-www-form-urlencoded
Instead of sending JSON, we construct the request body using:
new URLSearchParams({
...
})
This converts the data into the format Google expects.
During this request, we include:
- client_id
- client_secret
- redirect_uri
- authorization code
- code_verifier
- grant_type
The grant_type should be:
authorization_code
If everything is valid, Google returns an access token (and an ID token if requested).
Step 7: Retrieve User Information
Once we have the access token, we can request the authenticated user's profile.
Google provides a UserInfo endpoint:
https://www.googleapis.com/oauth2/v3/userinfo
Using the access token, we can retrieve information such as:
- User ID
- Name
- Profile Picture
- Email verification status
Step 8: Authenticate the User in Your Application
At this point, Google has successfully verified the user's identity.
Now it's our application's responsibility to determine whether this user already exists in our database.
Typically, we search using the user's email address or Google ID.
- If the user exists, log them in.
- If they don't exist, create a new account.
Finally, create your application's own session or JWT and redirect the user to the dashboard.
Final Flow
The complete Google OAuth flow now looks like this:
User clicks "Continue with Google"
│
▼
Generate state and PKCE verifier
│
▼
Generate code challenge
│
▼
Store state and verifier in cookies
│
▼
Redirect browser to Google's Authorization Endpoint
│
▼
User logs in and approves
│
▼
Google redirects to callback with code and state
│
▼
Validate state
│
▼
Exchange code + verifier for access token
│
▼
Request user information
│
▼
Create or verify user in database
│
▼
Create session
│
▼
Redirect to Dashboard
Conclusion
Although Google OAuth may seem complicated at first, it's really a sequence of well-defined steps.
The most important addition compared to providers like GitHub is PKCE, which adds another layer of protection by ensuring that only the application that initiated the authentication request can exchange the authorization code for an access token.
Once you understand the purpose of the state, code verifier, and code challenge, the entire flow becomes much easier to reason about.
Comments (0)
No comments yet. Be the first to share your thoughts.