DEV Community

Cover image for Integrating Google OAuth 2.0 with JWT in a Node.js + TypeScript App using Passport.js
fatih
fatih

Posted on

Integrating Google OAuth 2.0 with JWT in a Node.js + TypeScript App using Passport.js

Let’s cut to the chase—auth is a pain. And if you’ve ever used Passport.js, you know it’s not just a pain; it’s a full-blown headache. It’s powerful, yes, but also abstract and sometimes overly complex. But here’s the thing—if you want a solid auth flow, it’s worth the effort. Pair it with Google OAuth 2.0 and JWT, and you’re set with a secure and scalable solution that works. In this article, I’ll show you how to integrate Google OAuth 2.0 into a Node.js app with TypeScript, Passport, and JWT. By the end, you’ll have an auth setup that’s as good as it gets, without the usual "it only works on localhost" issues. Let’s get into it.

Before diving into the implementation, let me make one thing clear—I’m keeping this as real as it gets. We’re not just slapping together a "kinda-real" server; we’re building something solid, with best practices baked in. The goal here isn’t just to show you what to do, but also why we’re doing it. Let’s do this right.

Project Structure

Here’s the file structure, just to give you an idea of how it looks:

google-oauth-jwt-node/  
├── src/  
   ├── database/  
      ├── models/  
         └── User.ts  
   ├── middlewares/  
      └── requireJwt.ts  
   ├── auth/  
      ├── google.ts  
      ├── jwt.ts  
      └── passport.ts  
   ├── routes/  
      └── authRoute.ts  
      └── userRoute.ts  
   └── app.ts  
├── .env  
├── package.json  
├── tsconfig.json  
├── .gitignore  
Enter fullscreen mode Exit fullscreen mode

Let's initialize the project

First, let's create and initialize the Node.js project:

# you may use any name you want
mkdir google-oauth-jwt-node
cd google-oauth-jwt-node

npm init -y
Enter fullscreen mode Exit fullscreen mode

Second, we're going to install the dependencies that we are going to use:

  • express — Web framework for Node.js.
  • passport — Authentication middleware for Node.js.
  • passport-google-oauth20 — Passport strategy for Google OAuth 2.0 authentication.
  • passport-jwt — Passport strategy for JWT authentication, which will handle verifying JWT tokens.
  • jsonwebtoken — To generate and verify JWT tokens for authentication.
  • bcrypt — Library for hashing and comparing passwords.
  • uuid — For generating unique identifiers (we'll be using it for secure JWT tokens).
  • dotenv — To load environment variables from a .env file.
  • ts-node — TypeScript execution environment for running TypeScript files directly.
  • nodemon — Development tool to automatically restart the server when files change.

Don’t forget to install the @types/ packages as development dependencies for the ones that require them.

To install these, simply run:

# dependencies
npm install express passport passport-google-oauth20 passport-jwt jsonwebtoken bcrypt uuid dotenv

# devDependencies
npm install @types/node @types/express @types/passport @types/passport-google-oauth20 @types/passport-jwt @types/jsonwebtoken @types/bcrypt --save-dev

npm install nodemon typescript ts-node --save-dev
Enter fullscreen mode Exit fullscreen mode

Next, we need to initialize TypeScript:

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

Here’s a simple tsconfig.json configuration:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "baseUrl": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
Enter fullscreen mode Exit fullscreen mode

Let's not forget to create .gitignore:

node_modules/
.env
dist/
Enter fullscreen mode Exit fullscreen mode

Add these scripts to your package.json:

"scripts": {
  "dev": "nodemon src/app.ts",        // start in development with automatic restarts
  "build": "tsc"                      // to compile TypeScript
}
Enter fullscreen mode Exit fullscreen mode

Also, put these in your .env file:

GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
JWT_SECRET=your-jwt-secret
# you can change these with whatever you want
BE_BASE_URL=http://localhost:3000
FE_BASE_URL=http://localhost:3001
Enter fullscreen mode Exit fullscreen mode

Getting Google OAuth credentials (Client ID and Secret) is your responsibility. It’s a straightforward process, though it can be overwhelming if you’re unfamiliar with it. There are plenty of guides available online, so I’m not going to go over it here—I'll count on you to figure it out.

Just don't forget to add http://localhost:3000 as Authorized JavaScript origin and add http://localhost:3000/api/auth/google/callback as Authorized redirect URI.

Now we start to implement the project

Here is our app.ts:

// app.ts
import express, { Request, Response } from 'express';
import passport from './auth/passport';  // passport configuration
import dotenv from 'dotenv';
import { json } from 'body-parser';
import authRoute from './routes/authRoute';  // our authRoute
import userRoute from './routes/userRoute';  // our userRoute

// to load environment variables from .env file
dotenv.config();

const app = express();

// middleware to parse json bodies
app.use(json());

// authRoute
app.use('/api/auth', authRoute);

// userRoute
app.use('/api/user', userRoute);

// default route
app.get('/', (req: Request, res: Response) => {
  res.send('welcome to the Google OAuth 2.0 + JWT Node.js app!');
});

// start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`server is running on http://localhost:${PORT}`);
});

Enter fullscreen mode Exit fullscreen mode

Then, we start to implement the authentication strategies using Passport. These strategies include Google OAuth 2.0 for authenticating users with their Google accounts and JWT for securing routes with token-based authentication.

src/auth/passport.ts:

// passport.ts
import passport from 'passport';
import { googleStrategy } from './google';
import { jwtStrategy } from './jwt';

// initialize passport with Google and JWT strategies
passport.use('google', googleStrategy);
passport.use('jwtAuth', jwtStrategy);

export default passport;
Enter fullscreen mode Exit fullscreen mode

src/auth/google.ts:

// google.ts
import { Strategy as GoogleStrategy, Profile, VerifyCallback } from 'passport-google-oauth20';
import User from '../database/models/User'; // mock user class
import { v4 as uuidv4 } from 'uuid';

const options = {
  clientID: process.env.GOOGLE_CLIENT_ID || '',
  clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
  callbackURL: `${process.env.BE_BASE_URL}/api/auth/google/callback`,
};

async function verify(accessToken: string, refreshToken: string, profile: Profile, done: VerifyCallback) {
  try {
    // we check for if the user is present in our system/database.
    // which states that; is that a sign-up or sign-in?
    let user = await User.findOne({
      where: {
        googleId: profile.id,
      },
    });

    // if not
    if (!user) {
      // create new user if doesn't exist
      user = await User.create({
        googleId: profile.id,
        email: profile.emails?.[0]?.value,
        fullName: profile.displayName,
        jwtSecureCode: uuidv4(),
      });
    }

    // auth the User
    return done(null, user);
  } catch (error) {
    return done(error as Error);
  }
}

export default new GoogleStrategy(options, verify);
Enter fullscreen mode Exit fullscreen mode

src/auth/jwt.ts:

// jwt.ts
import { Strategy, ExtractJwt, VerifiedCallback } from 'passport-jwt';
import User from '../database/models/User'; // mock user class
import bcrypt from 'bcrypt';

const options = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.JWT_SECRET || 'secret-test',
};

async function verify(payload: any, done: VerifiedCallback) {
  /* 
    a valid JWT in our system must have `id` and `jwtSecureCode`.
    you can create your JWT like the way you like.
  */
  // bad path: JWT is not valid
  if (!payload?.id || !payload?.jwtSecureCode) {
    return done(null, false);
  }

  // try to find a User with the `id` in the JWT payload.
  const user = await User.findOne({
    where: {
      id: payload.id,
    },
  });

  // bad path: User is not found.
  if (!user) {
    return done(null, false);
  }

  // compare User's jwtSecureCode with the JWT's `jwtSecureCode` that the 
  // request has.
  // bad path: bad JWT, it sucks.
  if (!bcrypt.compareSync(user.jwtSecureCode, payload.jwtSecureCode)) {
    return done(null, false);
  }

  // happy path: JWT is valid, we auth the User.
  return done(null, user);
}

export default new Strategy(options, verify);
Enter fullscreen mode Exit fullscreen mode

Finally, we’ve set up the necessary strategies for Google OAuth2.0 and JWT authentication and instructed Passport.js to use them in passport.ts.

Now, let’s move on to securing our routes by implementing the requireJwt.ts middleware, which will ensure that access to protected routes is only granted with a valid accessToken.

src/middlewares/requireJwt.ts:

// requireJwt.ts
import passport from '../auth/passport';  // import passport from our custom passport file

// requireJwt middleware to authenticate the request using JWT
const requireJwt = passport.authenticate('jwtAuth', { session: false });

export default requireJwt;
Enter fullscreen mode Exit fullscreen mode

So far, we’ve set up the project, got Google OAuth2.0 and JWT working with Passport.js, and added the middleware to secure routes that need an access token. Now, we’ll implement the necessary endpoints in authRoute.ts to handle Google sign-ins and sign-ups for the app. We’ll also create a mock endpoint GET /api/user/ to return user info, which will only be accessible with a valid access token since we'll be adding our requireJwt.ts middleware in the route.

src/routes/authRoute.ts:

// authRoute.ts
import { Request, Response, Router } from 'express';
import passport from '../auth/passport';  // import passport from our custom passport file
import * as AuthService from '../services/AuthService';  // assuming you have a service

const router = Router();

/*
  This route triggers the Google sign-in/sign-up flow. 
  When the frontend calls it, the user will be redirected to the 
  Google accounts page to log in with their Google account.
*/
// Google OAuth2.0 route
router.get('/google', passport.authenticate('google', { scope: ['profile', 'email'] }));


/*
  This route is the callback endpoint for Google OAuth2.0. 
  After the user logs in via Google's authentication flow, they are redirected here.
  Passport.js processes the callback, attaches the user to req.user, and we handle 
  the access token generation and redirect the user to the frontend.
*/
// Google OAuth2.0 callback route
router.get('/google/callback', passport.authenticate('google', { session: false }), (req: Request, res: Response) => {
  try {
    // we can use req.user because the GoogleStrategy that we've 
    // implemented in `google.ts` attaches the user
    const user = req.user as User;

    // handle the google callback, generate auth token
    const { authToken } = AuthService.handleGoogleCallback({ id: user.id, jwtSecureCode: user.jwtSecureCode });

    // redirect to frontend with the accessToken as query param
    const redirectUrl = `${process.env.FE_BASE_URL}?accessToken=${authToken}`;
    return res.redirect(redirectUrl);
  } catch (error) {
    return res.status(500).json({ message: 'An error occurred during authentication', error });
  }
});

export default router;
Enter fullscreen mode Exit fullscreen mode

src/routes/userRoute.ts:

// userRoute.ts
import { Request, Response, Router } from 'express';
import requireJwt from '../middlewares/requireJwt'; // our middleware to authenticate using JWT
import UserService from '../services/UserService' // assuming you have a service

const router = Router();

// mock user info endpoint to return user data
router.get('/', requireJwt, (req: Request, res: Response) => {
  try {
    /* 
       The requireJwt middleware authenticates the request by verifying 
       the accessToken. Once authenticated, it attaches the User object 
       to req.user (see `jwt.ts`), making it availabe in the subsequent route handlers, 
       like those in userRoute.
    */
    // req.user is populated after passing through the requireJwt 
    // middleware
    const user = req.user as User;

    const veryVerySecretUserInfo = await UserService.getUser({ userId: user.id });

    // it is a mock, you MUST return only the necessary info :)
    return res.status(200).json(veryVerySecretUserInfo);
  } catch (error) {
    return res.status(500).json({ message: 'An error occurred while fetching user info', error });
  }
});

export default router;
Enter fullscreen mode Exit fullscreen mode

But... Fatih... How do I test it?

I hear you saying, "Fatih, this all sounds cool, but how do I know if it actually works?" Don’t worry, I got you. Here's how you can test everything locally: fire up your server, and follow my lead.

We first need to run the server, not actually fire it up... We're not arsonists, right?

Simply type:

npm run dev
Enter fullscreen mode Exit fullscreen mode

and you'll see the output server is running on http://localhost:3000

Now, we just need to open our browser and paste this:

localhost:3000/api/auth/google
Enter fullscreen mode Exit fullscreen mode

and if you did everything right, you'll see this screen;

google_signin_screen

This is essentially what happens when someone clicks the Sign up with Google button on your frontend.

When you select an account from that list, you’ll be redirected to localhost:3001/?accessToken=${accessToken}. Aaaaaand bada bing bada boom, your frontend will now need to handle this accessToken, save it in localStorage, and send a request to the GET api/user/ endpoint to get the necessary user info. After that, you can authorize the user.

Conclusion

And that’s pretty much it! You've got your Google sign-in and JWT setup working, now you can handle user authentication and authorization in your app. You’ve learned how to set up Passport with Google OAuth2.0, use JWT for session management, and protect routes with a simple middleware.

Next up, it’s just a matter of connecting the dots between the frontend and backend, handling the tokens properly, and giving users a smooth experience. You’re all set to keep building, so go ahead and test it out!

thank_you

I sincerely thank you for reading through, consider connecting with
me on;

youtube
x
linkedin
my personal website - fatihguzel.dev

Top comments (0)