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
- Operating System: macOS Monterey 12.3.1
- Node version: v16.16.0
- Passport version: [email protected]
- Passport-google-oauth2 version: [email protected]
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!