When it comes to authenticating users with Google, OAuth2 provides a secure and standardized approach. While there are libraries that abstract much of the process, this article walks you through a foundational implementation using JavaScript and an Express server. By the end, you'll understand the underlying concepts and have a running example of Google OAuth2.
The project can be run with any JavaScript runtime engine like Node or Deno, but we are using Bun (a fast, modern JavaScript runtime) to run our project.
Setting Up Google OAuth2
Before diving into the code, configure your project in the Google Cloud Console.
Step 1: Create a Google Cloud Project
- Visit Google Cloud Console.
- Create a new project if you do not have an existing one.
Step 2: Configure OAuth Consent Screen
- Navigate to API & Services > OAuth Consent Screen to configure your application.
- Fill in the necessary details like app name and support email.
- Customize branding (optional) to provide more details about your app’s consent screen.
Step 3: Set Up Scopes
- Under the Scopes section, add:
-
email
: Access the user’s email address. -
openid
: OpenID Connect scope for identity verification. -
profile
: Access basic profile information like name and picture.
-
Step 4: Create OAuth Client ID
It might take a few minutes before you can create a client ID.
- Go to Credentials > Create Credentials > OAuth Client ID.
- Select Web Application as the application type.
- Set the Authorized JavaScript Origin to
http://localhost:3000
. - Add a Redirect URI:
http://localhost:3000
. Replace these with your hosted URI when deploying to production. Google will redirect users to the Redirect URI after authentication, appending an authorization code and state parameters. The redirect URI can be another page or the backend service. For simplicity, we are using the same page to handle the authenticated user code.
Step 5: Add Test Users
In test mode, you must explicitly add the email addresses of users you want to test the flow with. You should publish your app once you're satisfied with the test.
Coding the Google OAuth2 Implementation
Project Setup
Install Bun:
If you don’t have Bun installed, follow the instructions at bun.sh. We are using Bun version 1.2.Initialize the Project:
Create a new directory for your project and initialize it with Bun:
mkdir google-oauth-express
cd google-oauth-express
bun init
Folder Structure
Organize your project as follows:
project/
├── public/
│ └── index.html
├── src/
│ └── index.js
├── .env
└── bun.lock
- Install Dependencies: Install the required dependencies:
bun add express google-auth-library
-
Set Environment Variables:
Create a
.env
file and add:
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URI=http://localhost:3000
index.html
The HTML file provides a simple interface for initiating Google Sign-In and handling the OAuth redirect.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Google OAuth Test App</title>
<script>
// Function to capture URL parameters and handle the OAuth redirect
function handleOAuthCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
const state = urlParams.get("state");
if (code && state) {
// Check the CSRF token
const storedState = localStorage.getItem("oauth_state");
if (state === storedState) {
// Send the code to the server for token exchange
fetch("/auth/google/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ code }),
})
.then((response) => response.json())
.then((data) => {
console.log("Success:", data);
})
.catch((error) => {
console.error("Error:", error);
});
} else {
console.error("State mismatch");
}
}
}
// Call the function when the page loads
handleOAuthCallback();
</script>
</head>
<body>
<h1>Google OAuth Test App</h1>
<button id="google-signin-button">Sign in with Google</button>
<script>
const GOOGLE_OAUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
const GOOGLE_CLIENT_ID = "your_google_client_id"; // Replace with your client ID
const GOOGLE_CALLBACK_URL = "http://localhost:3000";
const GOOGLE_OAUTH_SCOPES = [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
];
// Function to generate the OAuth URL and redirect the user
function initiateGoogleOAuth() {
const state = crypto.randomUUID(); // Generate a CSRF token
localStorage.setItem("oauth_state", state);
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: GOOGLE_CALLBACK_URL,
access_type: "offline",
response_type: "code",
state: state,
scope: GOOGLE_OAUTH_SCOPES.join(" "),
});
const GOOGLE_OAUTH_CONSENT_SCREEN_URL = `${GOOGLE_OAUTH_URL}?${params.toString()}`;
window.location.href = GOOGLE_OAUTH_CONSENT_SCREEN_URL;
}
// Attach the function to the button click event
document
.getElementById("google-signin-button")
.addEventListener("click", initiateGoogleOAuth);
</script>
</body>
</html>
index.js
This file handles the server-side implementation of Google OAuth2.
import express from "express";
import { OAuth2Client } from "google-auth-library";
const app = express();
app.use(express.static("public"));
app.use(express.json());
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const GOOGLE_REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI;
if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET || !GOOGLE_REDIRECT_URI) {
throw new Error("Missing required environment variables for Google OAuth.");
}
const googleClient = new OAuth2Client(GOOGLE_CLIENT_ID);
// Middleware to check if the user is authenticated
async function isAuthenticated(req, res, next) {
const token = req.headers.authorization?.split(" ")[1]; // Extract token from "Bearer <token>"
if (!token) {
return res.status(401).json({ message: "Unauthorized: No token provided" });
}
try {
const ticket = await googleClient.verifyIdToken({
idToken: token,
audience: GOOGLE_CLIENT_ID,
});
const payload = ticket.getPayload();
req.user = payload;
next();
} catch (error) {
console.error("Token verification failed:", error);
res.status(401).json({ message: "Unauthorized: Invalid token" });
}
}
// Route to handle Google OAuth login
app.post("/auth/google/login", async (req, res) => {
const { code } = req.body;
if (!code) {
return res.status(401).json({ message: "Unauthorized" });
}
try {
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: GOOGLE_REDIRECT_URI,
grant_type: "authorization_code",
}),
}).then((res) => res.json());
res.json({ token: response.id_token });
} catch (error) {
console.error("Error during token exchange:", error);
res.status(500).json({ message: "Failed to authenticate with Google" });
}
});
// Protected route example
app.get("/protected", isAuthenticated, (req, res) => {
res.json({ message: "You are authenticated!", user: req.user });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
Running the Project
- Run the Server: Execute the following command:
bun run src/index.js
-
Test the Flow:
- Open
http://localhost:3000
in your browser. - Click Sign in with Google.
- Authenticate with your Google account.
- Check the console for the token.
- Use the bearer token to access the
/protected
route.
- Open
Conclusion
This implementation provides a solid understanding of Google OAuth2 and can be ported to other frameworks or production-ready applications. While tools like passport
or next-auth
simplify OAuth2, building it manually ensures you understand how authentication works under the hood.
Top comments (0)