← Back to list

Google login in Express.js

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

Jose Garcia · 2026-01-20 16:08 · 0 claps · 8.0 min read
#expressjs #google-login
Open on Medium ↗
Wiki topics: 🌐 · Web Development 📰 · Journalism & News

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

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.

  1. Getting Google client ID and secret.
  2. Front-end custom button.
  3. 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 OAuth2Client from 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_ID variable 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.

  1. It is stateless.
  2. 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:

  1. nonce means “number once”. We are storing a string, but hey, let me know in the comments if you have a better name for it.
  2. iat means “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.

  1. It has a valid signature.
  2. 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