Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERR_HTTP_HEADERS_SENT Error in Passport Google OAuth 2.0 Authentication #94

Open
learnyx-ai opened this issue Aug 1, 2023 · 1 comment

Comments

@learnyx-ai
Copy link

Issue: ERR_HTTP_HEADERS_SENT Error in Passport Google OAuth 2.0 Authentication

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.

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'm unable to pinpoint the reason why this error occurs. I'm looking for any advice on how to troubleshoot or fix this issue. Thanks for any help or suggestions!

@jhargett1
Copy link

The issue is likely occurring because you are calling res.redirect() directly within the Passport authenticate callback function. This will cause headers to be sent twice - once from the redirect and once from the original request handler.

A few ways to fix:

  1. Don't call res.redirect() in the callback. Instead, just call done() and let the request handler send the response:
this.passportService.authenticate(
  EnabledAuthenticationStrategies.GOOGLE, 
  {
    failureRedirect: "/login",
    successReturnToOrRedirect: `${CURRENT_CLIENT_HOST_URL}/auth/google/success`,
  },
  (error, user, info) => {
    // Remove res.redirect()
    done(error, user, info); 
  }
)(req, res, next);

Then in your request handler:

if(req.user) {
  // User authenticated, redirect
  res.redirect(`${CURRENT_CLIENT_HOST_URL}/auth/google/success`); 
} else {
  // Auth failed, handle error
}
  1. Use the successRedirect option instead of successReturnToOrRedirect. This will handle the redirect internally:
this.passportService.authenticate(
  EnabledAuthenticationStrategies.GOOGLE,
  {
    failureRedirect: "/login", 
    successRedirect: `${CURRENT_CLIENT_HOST_URL}/auth/google/success`,
  }
)(req, res, next);
  1. Call next() instead of redirecting. This will skip to the next request handler:
this.passportService.authenticate(
  EnabledAuthenticationStrategies.GOOGLE,
  (error, user, info) => {
    return next();
  }
)(req, res, next);

Then handle the redirect there if needed.

Let me know if any of those suggestions help resolve it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants