Google login in Express.js
A guide on how to implement Google Login in Express.js. Without relying on the Sign In button.
Google login in Express.js
A guide on how to implement Google Login in Express.js. Without relying on the Sign In button.

Image by ChatGPT
Login in Google is a standard at this point. How do we do it?
In this guide, I will cover how to implement Google Login. I will not use the famous Google Sign In button. I divided this guide into 3 parts.
- Getting Google client ID and secret.
- Front-end custom button.
- Back-end OAuth client.
I am not a Sign In button hater. You know who is, though? Your team’s designer. So, better to know how to do it from scratch.
In this guide, I assume you:
- Have a Google Cloud account. You need it for the sign-in.
- Know the basics of Express. This works in versions 4 and 5.
- Know the basics of HTML and JavaScript.
I use module files in this guide. That is why you will see import imports and .mjs extensions.
Let’s get those Google credentials.
Create a Google Client
To get the Google credentials, we first create a Google Client. We make Google clients from the Google Cloud console. From the client, we get the necessary credentials.
We need the client ID and secret. To get them, go to the “Clients” service.

Click on the “Clients” tab.

Click on the “+ Create client” button.

Select “Web application” at the “Application Type” drop-down.

Set your origin at the “Authorized JavaScript origins” field. You need to click on the “+ Add URI”.
Also set the “Authorized redirect URIs”.

You can use localhost with Google. Useful for development testing.
Once you create the service, the following pop-up will appear.

Copy the client ID and secret and click “OK”.
Now, let’s do our simple front-end.
Frontend
Let’s keep this FE super simple. This way, it’s usable in any framework.
In an index.html (or any HTML page for that matter), add.
<button id="button-google-login">Log in with Google</button>
<script>
window.addEventListener("DOMContentLoaded", () => {
const googleLoginButton = document.getElementById("button-google-login");
googleLoginButton.addEventListener("click", () => {
window.location.href = `http://localhost:3000/auth/google`;
});
});
</script>
We are creating a button and attaching a click event.
When you click on the button, Google will redirect you to the callback route on our server.
Setup
Before we make the back end, we need to set up a few things on our Express project.
In a .env file, add.
APP_URL=http://localhost:3000
GOOGLE_ID=<<your google ID>>
GOOGLE_SECRET=<<your google secret>>
GOOGLE_STATE_SECRET=<<strong secret>>
Don’t forget to set the correct value for each variable.
We make the GOOGLE_STATE_SECRET variable ourselves. For testing purposes, you can put anything there. The best practice is to generate a 256-bit random string.

We need a cookie parser for Express. Check this NPM module.
Install Google’s helper library.
npm install google-auth-library
This library simplifies communication with Google’s OAuth implementation. It’s not necessary, but I don’t want this guide to be too long.
Router
In a routes/index.mjs add.
import { Router } from "express";
import { OAuth2Client } from "google-auth-library";
import crypto from "crypto";
let router = Router();
const { APP_URL, GOOGLE_ID, GOOGLE_SECRET, GOOGLE_STATE_SECRET } = process.env;
const REDIRECT_URI = `${APP_URL}auth/google/callback`;
const googleClient = new OAuth2Client({
clientId: GOOGLE_ID,
clientSecret: GOOGLE_SECRET,
redirectUri: REDIRECT_URI,
});
We are importing.
- The express
Router. - The
OAuth2Clientfrom the Google library helper. - The
crypto. - The environment variables.
Notice how you create a Google client.
const googleClient = new OAuth2Client({
clientId: GOOGLE_ID,
clientSecret: GOOGLE_SECRET,
redirectUri: REDIRECT_URI,
});
With that out of the way, let’s make our back end.
Google login
The Google login has two steps.
- Redirection to Google’s authentication page.
- Handling the response of Google’s auth page.
Redirect to Google’s authentication page
In your routes/index.mjs file, add:
router.get("/auth/google", (_, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_ID,
redirect_uri: REDIRECT_URI,
response_type: "code",
scope: "openid email profile",
prompt: "select_account",
state: makeState(),
});
const GOOGLE_URL = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
return res.redirect(GOOGLE_URL);
});
function makeState() {
const payload = {
nonce: crypto.randomBytes(16).toString("hex"),
iat: Date.now(),
};
const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
const sig = crypto
.createHmac("sha256", STATE_SECRET)
.update(encoded)
.digest("base64url");
return `${encoded}.${sig}`;
}
Notice that this method is a GET. The endpoint redirects to Google’s login page. So, we don’t need it to be a POST.
Make the query parameters
router.get("/auth/google", (_, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_ID,
redirect_uri: REDIRECT_URI,
response_type: "code",
scope: "openid email profile",
prompt: "select_account",
state: makeState(),
});
...
});
Google’s OAuth documentation defines a series of arguments that we must send. These are.
client_id. TheGOOGLE_IDvariable we defined in the .env.redirect_uri. Our own endpoint to which Google’s login view will redirect.response_type. Check the options in the docs. Code is the recommended one.scope. To request access to the user’s account. In this case, we are requesting access to the email and profile.prompt. Tells Google which type of prompt to show to the user. In this case, the select account page.state. A value to avoid Cross-Site Request Forgery (CSRF) attacks. I will cover it later in the guide.
Redirect the user to the Google screen
const GOOGLE_URL = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
return res.redirect(GOOGLE_URL);
Make state
Let’s talk more about the state query param.
Google defines an optional state variable. This is useful to avoid CSRF attacks. Explaining CSRF attacks is outside the scope of this guide.
There is more than one way to implement the state. And where there is a difference, there is a holy war. I will make a base 64 encoded token. To add to the security, I will sign it with HMAC. This has two advantages.
- It is stateless.
- It’s agnostic. You can use it with your classic app or a REST API.
Let’s go over the implementation
Make payload for hmac
function makeState() {
const payload = {
nonce: crypto.randomBytes(16).toString("hex"),
iat: Date.now(),
};
...
}
We will make a payload object. This object will contain two attributes:
noncemeans “number once”. We are storing a string, but hey, let me know in the comments if you have a better name for it.iatmeans “issued at”. We are storing the timestamp. With this, we can determine if the token has expired.
We use crypto.randomBytes() to generate a random number for the nonce. And cast it to a string in the hexadecimal format. The method randomBytes() returns a buffer. If we don’t cast it to string it will fail.
Make token
const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
Encodes the payload to base64. Remember that we are sending this state in the query string. The base64url format encodes the object into a string compatible with the URL format.
const sig = crypto
.createHmac("sha256", STATE_SECRET)
.update(encoded)
.digest("base64url");
return `${encoded}.${sig}`;
We use HMAC to sign the encoded payload. If an attacker modifies the payload, the signature will be invalid. That is, unless he manages to get your secret.
And we return the encoded value alongside its signature.
Google callback
This is the route to which Google’s login will redirect the user.
Into your routers/index.mjs add
router.get("/auth/google/callback", async (req, res, next) => {
const { code, state } = req.query;
if (!code || !state) return next(new Error("Missing Google credential"));
const isValid = validateState(state);
if (!isValid) return next(new Error("Invalid state"));
try {
const { tokens } = await googleClient.getToken(String(code));
if (!tokens.id_token) throw new Error("Error with Google Login`");
const ticket = await googleClient.verifyIdToken({
idToken: tokens.id_token,
audience: GOOGLE_ID,
});
const payload = ticket.getPayload();
const { email, name, picture } = payload;
console.log("Logged in successfully");
console.log(payload);
return res.redirect(APP_URL);
} catch (error) {
return next(error);
}
});
function validateState(state) {
const [encoded, sig] = state.split(".");
const expectedSig = crypto
.createHmac("sha256", STATE_SECRET)
.update(encoded)
.digest("base64url");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) {
return false;
}
const payload = JSON.parse(Buffer.from(encoded, "base64url").toString());
const FIVE_MINUTES = 5 * 60 * 1000;
if (Date.now() - payload.iat > FIVE_MINUTES) {
return false;
}
return true;
}
Check that the code and the state are in the petition
const { code, state } = req.query;
if (!code || !state) return next(new Error("Missing Google credential"));
First is checking if both the code and state are in the request’s query parameters. Otherwise, pass an error to the next() error handler.
If you don’t know how to implement an error handler, check this article.
Notice that in Express v5, you don’t need to call the next() function. You can throw the error, and Express will pass it to the error handler.
Validate state
const isValid = validateState(state);
if (!isValid) return next(new Error("Invalid state"));
The next step is to validate the state. I cover the validateState() function later on.
Get tokens
try {
const { tokens } = await googleClient.getToken(String(code));
if (!tokens.id_token) throw new Error("Error with Google Login`");
...
} ...
We ask Google for the tokens. With the code, you can’t access Google’s info. We only use the code to get the tokens.
Get user info
const ticket = await googleClient.verifyIdToken({
idToken: tokens.id_token,
audience: GOOGLE_ID,
});
const payload = ticket.getPayload();
const { email, name, picture } = payload;
We call the verifyIdToken() method to get the object that contains the info we need. Next, we call the getPayload() method on the info.
Mock login
console.log("Logged in successfully");
console.log(payload);
return res.redirect(APP_URL);
At this point, we managed to get Google’s information. Now we should either make our own tokens in a REST API or a session in a traditional app. This blasted article is too long, so I won’t cover that. Let me know in the comments if you want me to write a guide for either.
Validate function
The validate function validates the state. For a state to be valid, it has to meet two conditions.
- It has a valid signature.
- It hasn’t expired.
function validateState(state) {
const [encoded, sig] = state.split(".");
const expectedSig = crypto
.createHmac("sha256", STATE_SECRET)
.update(encoded)
.digest("base64url");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) {
return false;
}
const payload = JSON.parse(Buffer.from(encoded, "base64url").toString());
const FIVE_MINUTES = 5 * 60 * 1000;
if (Date.now() - payload.iat > FIVE_MINUTES) {
return false;
}
return true;
}
Verifying the signature
const expectedSig = crypto
.createHmac("sha256", STATE_SECRET)
.update(encoded)
.digest("base64url");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) {
return false;
}
We first make the expected signature. You will recognize this, as it’s the same as the makeState() function.
The crypto package has the timingSafeEqual to compare different signatures.
We return false if it’s not valid.
If it’s true, we need to check that it hasn’t expired.
Checking the expiration date
const payload = JSON.parse(Buffer.from(encoded, "base64url").toString());
const FIVE_MINUTES = 5 * 60 * 1000;
if (Date.now() - payload.iat > FIVE_MINUTES) {
return false;
}
We decode the payload to get the iat value. If more than five minutes have passed, we return false.
Conclusions
In this guide, I covered how to implement Google login in Express.js. We covered the entire process login process. A process which step by step is:
- Getting the Google ID and secret.
- A simple frontend.
- Implementing the logic to obtain the credentials.
- Implementing the logic to avoid CSRF attacks.
I hope this was useful to you. Before you go, if you want to implement login with Discord, check this other guide. Until next time.
메타데이터
- post_id
- cfa1034b7767
- slug
- google-login-in-express-js-cfa1034b7767
- url
- https://medium.com/@jogarcia/google-login-in-express-js-cfa1034b7767
- canonical_url
- https://medium.com/@jogarcia/google-login-in-express-js-cfa1034b7767
- author_url
- https://medium.com/@jogarcia
- status
- ok
- fetched_at
- 2026-06-21 22:26:41