How to resolve 'ERR_HTTP_HEADERS_SENT' Error in Passport Google OAuth 2.0 Authentication?

39 Views Asked by At

Expected behavior

Upon successful authentication using Google OAuth 2.0 with Passport, I expect the user to be redirected to a successful login route.

Actual behavior

I'm getting the Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client when trying to redirect after successful authentication using Google OAuth 2.0 with Passport. The error seems to originate from response.redirect within the handleAuthenticationWithGoogleSuccess function.

I've also tried using successReturnToOrRedirect: `${CURRENT_CLIENT_HOST_URL}/auth/google/success` without getting any other result. If I log the user within the callback, it is being returned...

Steps to reproduce

Here are the main snippets from my code that relate to this issue:

Passport Service

import "reflect-metadata";
import { inject, injectable } from "inversify";
import passport from "passport";
import { PassportGoogleStrategy } from "@authentication/application/strategies/google.strategy";
import { UserManagementModuleIdentifiers } from "@userManagement/dependencies/identifiers";
import { IPassportService } from "../interfaces/services.interfaces";
import { PassportLocalStrategy } from "@authentication/application/strategies/local.strategy";
import { IGetUserUseCase } from "@userManagement/modules/users/domain/interfaces/usecases.interfaces";
import { UserEntity } from "@userManagement/modules/users/domain/entities/user.entity";

export enum EnabledAuthenticationStrategies {
  GOOGLE = "google",
  JWT = "jwt",
  LOCAL = "local",
}

@injectable()
export class PassportService implements IPassportService {
  constructor(
    @inject(UserManagementModuleIdentifiers.PASSPORT_GOOGLE_STRATEGY)
    private readonly googleStrategy: PassportGoogleStrategy,
    @inject(UserManagementModuleIdentifiers.PASSPORT_LOCAL_STRATEGY)
    private readonly localStrategy: PassportLocalStrategy,
    @inject(UserManagementModuleIdentifiers.GET_USER_USE_CASE)
    private readonly getUserUseCase: IGetUserUseCase
  ) {
    passport.use(
      EnabledAuthenticationStrategies.GOOGLE,
      this.googleStrategy.getStrategy()
    );
    passport.use(
      EnabledAuthenticationStrategies.LOCAL,
      this.localStrategy.getStrategy()
    );

    passport.serializeUser(function (user: UserEntity, done) {
      return done(null, user.guid);
    });

    passport.deserializeUser((guid: string, done) => {
      this.getUserUseCase
        .execute(guid)
        .then((user) => {
          console.log("Deserialized User:", user);
          return done(null, user);
        })
        .catch((err) => {
          return done(err, null);
        });
    });
  }

  initialize = () => {
    return passport.initialize();
  };

  session = () => {
    return passport.session();
  };

  authenticate = (strategy: string, options?: object, done?: any) => {
    return passport.authenticate(strategy, options, done);
  };
}

Google Strategy

import { injectable, inject } from "inversify";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import {
  ICreateUserUseCase,
  ISearchUserUseCase,
} from "@userManagement/modules/users/domain/interfaces/usecases.interfaces";
import { UserManagementModuleIdentifiers } from "@userManagement/dependencies/identifiers";
import { IUserCreatedFromValidSources } from "@userManagement/modules/users/domain/entities/user.entity";
import {
  CURRENT_SERVER_HOST_URL,
  GOOGLE_CLIENT_ID,
  GOOGLE_CLIENT_SECRET,
} from "@shared/infrastructure/config/env";

@injectable()
export class PassportGoogleStrategy {
  private strategy: GoogleStrategy;

  constructor(
    @inject(UserManagementModuleIdentifiers.CREATE_USER_USE_CASE)
    private readonly createUserUseCase: ICreateUserUseCase,
    @inject(UserManagementModuleIdentifiers.SEARCH_USER_USE_CASE)
    private readonly searchUserUseCase: ISearchUserUseCase
  ) {
    this.strategy = new GoogleStrategy(
      {
        clientID: GOOGLE_CLIENT_ID,
        clientSecret: GOOGLE_CLIENT_SECRET,
        callbackURL: `${CURRENT_SERVER_HOST_URL}/auth/google/success`,
      },
      async (_, __, profile, done) => {
        // Use arrow function here
        try {
          let user = await this.searchUserUseCase.execute({
            email: profile.emails[0].value,
          });

          if (!user) {
            user = await this.createUserUseCase.execute({
              email: profile.emails[0].value,
              createdFrom: IUserCreatedFromValidSources.GOOGLE,
            });
          }

          return done(null, user);
        } catch (err) {
          return done(err);
        }
      }
    );
  }

  getStrategy() {
    return this.strategy;
  }
}

Controller

import {
  controller,
  httpPost,
  BaseHttpController,
  httpGet,
  request,
  response,
  next,
} from "inversify-express-utils";
import { inject } from "inversify";
import { UserManagementModuleIdentifiers } from "@userManagement/dependencies/identifiers";
import {
  ISignUpHandler,
  ILoginHandler,
} from "@authentication/domain/interfaces/handlers.interfaces";
import { verifyApiKeyMiddleware } from "@authorization/presentation/middlewares/valid-api-key.middleware";
import { IPassportService } from "@authentication/domain/interfaces/services.interfaces";
import { NextFunction, Request, Response } from "express";
import { EnabledAuthenticationStrategies } from "@authentication/domain/services/passport.service";
import { IAuthenticationController } from "@authentication/domain/interfaces/controllers.interfaces";
import { CURRENT_CLIENT_HOST_URL } from "@shared/infrastructure/config/env";

export function sessionLogger(req: Request, res: Response, next: NextFunction) {
  // Log session ID and user information (if available) for each request
  console.log("Session ID:", (req as any).sessionID);
  console.log("User:", req.user);

  next(); // Call the next middleware in the chain
}

@controller("/auth")
export class AuthenticationController
  extends BaseHttpController
  implements IAuthenticationController
{
  constructor(
    @inject(UserManagementModuleIdentifiers.SIGN_UP_HANDLER)
    private signUpHandler: ISignUpHandler,
    @inject(UserManagementModuleIdentifiers.LOGIN_HANDLER)
    private loginHandler: ILoginHandler,
    @inject(UserManagementModuleIdentifiers.PASSPORT_SERVICE)
    private passportService: IPassportService
  ) {
    super();
  }

  @httpPost("/signup", verifyApiKeyMiddleware)
  signUp() {
    return this.signUpHandler.handle(
      this.httpContext.request,
      this.httpContext.response
    );
  }

  @httpPost("/login")
  login() {
    return this.loginHandler.handle(
      this.httpContext.request,
      this.httpContext.response
    );
  }

  @httpGet("/google")
  authenticateWithGoogle(req: Request, res: Response, next: NextFunction) {
    return this.passportService.authenticate(
      EnabledAuthenticationStrategies.GOOGLE,
      { scope: ["profile", "email"] }
    )(req as any, res, next);
  }

  @httpGet("/google/success")
  handleAuthenticationWithGoogleSuccess(
    @request() req: Request,
    @response() res: Response,
    @next() next: NextFunction
  ) {
    this.passportService.authenticate(
      EnabledAuthenticationStrategies.GOOGLE,
      {
        failureRedirect: "/login",
        successReturnToOrRedirect: `${CURRENT_CLIENT_HOST_URL}/auth/google/success`,
      },
      (error, user, info) => {
        // here use res directly, not via the callback args
        res.redirect(`${CURRENT_CLIENT_HOST_URL}/auth/google/success`);
      }
    )(req as any, res, next);
  }

  @httpGet("/me")
  getCurrentUser(req: Request, res: Response) {
    // Log session information using the sessionLogger middleware
    sessionLogger(req, res, () => {
      if (req.isAuthenticated()) {
        // User is authenticated, return the user's information
        const user = req.user;
        res.json(user);
      } else {
        // User is not authenticated, return an appropriate response
        res.status(401).json({ error: "Not authenticated" });
      }
    });
  }

  @httpPost("/logout")
  logout(req: Request) {
    req.logout((err: any) => {
      if (err) {
        // Handle error if needed
        console.error("Error occurred during logout:", err);
      }
    });
  }
}

server.ts

import "reflect-metadata";
import * as Sentry from "@sentry/node";
import {
  connectCriticalInfrastructure,
  disconnectCriticalInfrastructure,
} from "@shared/infrastructure/helpers/critical-infrastructure.helpers";
import { setupGracefulShutdown } from "@shared/infrastructure/helpers/server.helpers";
import { errorHandler } from "@shared/presentation/middlewares/error-handling.middleware";
import cors from "cors";
import express from "express";
import helmet from "helmet";
import morgan from "morgan";
import session from "express-session";

import { IPassportService } from "@authentication/domain/interfaces/services.interfaces";
import { GlobalDependenciesIdentifiers } from "@shared/infrastructure/dependencies/identifiers";
import { getRootContainer } from "@shared/infrastructure/dependencies/root-container";
import { initializeLoggingInfrastructure } from "@shared/infrastructure/helpers/secondary-infrastructure.helpers";
import { InversifyExpressServer } from "inversify-express-utils";
import { mainRouter } from "shared/presentation/routes/main-router";
import { JWT_SECRET } from "@shared/infrastructure/config/env";

const PORT = process.env.PORT || 3000;
let server = new InversifyExpressServer(getRootContainer());

server.setConfig((app) => {
  initializeLoggingInfrastructure(app);

  const passport = getRootContainer().get<IPassportService>(
    GlobalDependenciesIdentifiers.USER_MANAGEMENT.PASSPORT_SERVICE
  );
  passport.initialize(); // Initialize Passport

  // Middlewares
  app.use(
    session({
      secret: JWT_SECRET,
      resave: false,
      saveUninitialized: false,
      cookie: { secure: process.env.NODE_ENV === 'production' },
    })
  );
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));
  app.use(morgan("dev"));
  app.use(helmet());
  app.use(cors());
  app.use(passport.session());
  app.use("/", mainRouter);
  
  app.use(Sentry.Handlers.errorHandler());
  app.use(errorHandler);

  // Start server
  connectCriticalInfrastructure()
    .catch(async (error) => {
      console.error(
        "AN ERROR OCCURED WHILE CONNECTING CRITICAL INFRASTRUCTURE DEPENDENCIES: ",
        error
      );
      await disconnectCriticalInfrastructure();
      process.exit(1);
    })
    .then(() => {
      const server = app.listen(PORT, () => {
        console.info(`Server is listening on port ${PORT}.`);
      });

      // Setting up graceful shutdown
      setupGracefulShutdown(server, disconnectCriticalInfrastructure);
    });
});

server.build();

Environment

I have attempted to troubleshoot the issue myself but have been unable to identify the root cause. I suspect the error may be related to the order of setting headers or the usage of Passport middleware in the handleAuthenticationWithGoogleSuccess function.

I am seeking advice, solutions, or any suggestions on how to resolve the 'ERR_HTTP_HEADERS_SENT' error and achieve successful authentication using Google OAuth 2.0 with Passport. Any help would be greatly appreciated! Thank you!

0

There are 0 best solutions below