I'm currently facing an issue with my Nest.js application that utilizes Google OAuth for authentication. The Nest.js backend is running on port 5000, and the frontend (Next.js) is running on port 3000. I've set up the Google Cloud Console with the correct origin URI and redirect URI, both set to "http://localhost:3000".
I am facing below error in frontend Next.js
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000&scope=email%20profile&client_id=${GOOGLE_OAUTH_CLIENT_ID}' (redirected from 'http://localhost:5000/auth/google') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
GET https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000&scope=email%20profile&client_id=${GOOGLE_OAUTH_CLIENT_ID} net::ERR_FAILED 302 (Found)
Here's a snippet of my Nest.js Auth Controller, Google OAuth Strategy, and Google OAuth Guard:
// Nest.js google oauth strategy
export class GoogleOAuthStrategy extends PassportStrategy(Strategy, 'google') {
constructor(private readonly configService: ConfigService) {
super({
clientID: configService.get('googleOAuthClientId'),
clientSecret: configService.get('googleOAuthClientSecret'),
callbackURL: configService.get('googleOAuthCallbackURL'),
scope: ['email', 'profile'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: Profile,
done: VerifyCallback,
): Promise<any> {
const { id, name, emails, photos } = profile;
const user: User = {
type: 'individual',
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
picture: photos[0].value,
authenticationProviders: [{ name: 'google', id }],
};
done(null, user);
}
}
// Nest.js google oauth guard
export class GoogleOAuthGuard extends AuthGuard('google') {
constructor() {
super({
accessType: 'offline',
});
}
}
// Nest.js auth controller
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Get('google')
@UseGuards(GoogleOAuthGuard)
async googleAuth() {
return HttpStatus.OK;
}
@Get('google-redirect')
@UseGuards(GoogleOAuthGuard)
googleAuthRedirect(
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
) {
return this.authService.login(req, res, 'google');
}
}
// Nest.js auth service
export class AuthService {
constructor(
private readonly configService: ConfigService,
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async login(req: Request, res: Response, provider: Provider): Promise<void> {
const user = req.user as User;
if (!user) {
throw new NotFoundException(`No user from ${provider}`);
}
let userPayload: User;
let accessTokenPayload: string;
const foundUser = await this.usersService.findByEmail(user.email);
if (foundUser) {
const providerExists = foundUser.authenticationProviders.some(
(provider) => provider.name === user.authenticationProviders[0].name,
);
// User found with different provider
if (!providerExists) {
foundUser.authenticationProviders.push(user.authenticationProviders[0]);
await foundUser.save();
}
userPayload = foundUser;
accessTokenPayload = foundUser._id.toString();
} else {
// Save user to mongodb if it does not exists already
const newUser = await this.usersService.create(user);
userPayload = newUser;
accessTokenPayload = newUser._id.toString();
}
const accessToken = this.jwtService.sign({ id: accessTokenPayload });
res.cookie('CARDTRIKA_ACCESS_TOKEN', accessToken, {
maxAge: this.configService.get('cookieMaxAge'),
httpOnly: true,
});
res.status(HttpStatus.OK).json({
statusCode: HttpStatus.OK,
success: true,
message: 'User information',
data: userPayload,
});
}
}
If I make get request to http:localhost:5000/auth/google directly from the browser it redirects to google api > consent screen and then I can login through gmail account and finally can get user information successfully.
But the same is not happening with Next.js. As soon as I make request it says CORS error.
SOLUTION:
So, basically there are two endpoints. /auth/google and /auth/google-redirect. And on google console, origin-uri should be http://localhost:3000 and redirect-uri should be set to http://localhost:3000/auth/google-redirect(wherever we want to manage in frontend).
Now we have to request to /auth/google from origin localhost:5000. So I made it as below,
const handleLogin = async (method: string) => {
window.location.replace(`${apiBaseUrl}/auth/${method.toLowerCase()}`);
};
So it opens up consent screen and after logging in it will redirect to http://localhost:3000/auth/google-redirect?code=<some-code-and-other-things> and it frontend I have managed this url page and made another request to http://localhost:5000/auth/google-redirect with queryString. So in backend now, the google strategy know it's authenticated due to the known code it provided and give us user's details.