Im building a backend with nestjs and mongodb with regular authentication and google authentication. The problem is when user signs up with regular authentication and after tries to login with google it passes. What is the best practice to prevent that?
I tried throw an error if user is in db and user authprovider is ‘local’ but it doesnt seem to work.
Im posting my code in here. Hope you can help me.
auth.service.ts
BadRequestException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { compare, hash } from 'bcryptjs';
import { Response } from 'express';
import { User } from 'src/users/schema/user.schema';
import { TokenPayload } from './token-payload.interface';
import { SignupDto } from './dtos/signup.dto';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { UsersService } from 'src/users/users.service';
import { ResetToken } from './schemas/reset-token.schema';
const { nanoid } = require('nanoid');
import { MailService } from './services/mail.service';
@Injectable()
export class AuthService {
constructor(
@InjectModel(User.name) private readonly userModel: Model<User>,
@InjectModel(ResetToken.name) private resetTokenModel: Model<ResetToken>,
private readonly usersService: UsersService,
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
private mailService: MailService,
) {}
async login(user: User, response: Response, redirect = false) {
//calculate expiration date for access token
const expiresAcessToken = new Date();
expiresAcessToken.setMilliseconds(
expiresAcessToken.getTime() +
parseInt(
this.configService.getOrThrow('JWT_ACCESS_TOKEN_EXPIRATION_MS'),
),
);
//calculate expiration date for refresh token
const expiresRefreshToken = new Date();
expiresRefreshToken.setMilliseconds(
expiresRefreshToken.getTime() +
parseInt(
this.configService.getOrThrow('JWT_REFRESH_TOKEN_EXPIRATION_MS'),
),
);
const tokenPayload: TokenPayload = {
userId: user._id.toHexString(),
};
//create access token
const accessToken = this.jwtService.sign(tokenPayload, {
secret: this.configService.getOrThrow('JWT_ACCESS_TOKEN_SECRET'),
expiresIn: `${this.configService.getOrThrow('JWT_ACCESS_TOKEN_EXPIRATION_MS')}ms`,
});
//create refresh token
const refreshToken = this.jwtService.sign(tokenPayload, {
secret: this.configService.getOrThrow('JWT_REFRESH_TOKEN_SECRET'),
expiresIn: `${this.configService.getOrThrow('JWT_REFRESH_TOKEN_EXPIRATION_MS')}ms`,
});
await this.usersService.updateUser(
{
_id: user._id,
},
{
$set: { refreshToken: await hash(refreshToken, 10) },
},
);
response.cookie('access_token', accessToken, {
httpOnly: true,
secure: this.configService.getOrThrow('NODE_ENV') === 'production',
expires: expiresAcessToken,
});
response.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: this.configService.getOrThrow('NODE_ENV') === 'production',
expires: expiresRefreshToken,
});
if (redirect)
response.redirect(this.configService.getOrThrow('FRONTEND_URL'));
else
response.status(200).json({
_id: user._id,
name: user.name,
email: user.email,
});
}
async verifyUser(email: string, password: string) {
const user = await this.usersService.getUser({ email });
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
if (user.authProvider !== 'local') {
console.log('User is not local');
throw new UnauthorizedException('No account associated with this email for password login. Please use Google login.');
}
const authenticated = await compare(password, user.password);
if (!authenticated) {
throw new UnauthorizedException('Invalid credentials');
}
return user;
}
}
auth.controller.ts
import {
Body,
Controller,
Get,
Post,
Put,
Res,
UseGuards,
} from '@nestjs/common';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { CurrentUser } from './current-user.decorator';
import { User } from 'src/users/schema/user.schema';
import { Response } from 'express';
import { AuthService } from './auth.service';
import { JwtRefreshAuthGuard } from './guards/jwt-refresh-auth.guard';
import { GoogleAuthGuard } from './guards/google-auth.guard';
import { SignupDto } from './dtos/signup.dto';
import { ChangePasswordDto } from './dtos/change-password.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { ForgotPasswordDto } from './dtos/forgot-password.dto,';
import { ResetPasswordDto } from './dtos/reset-password.dto';
import { ConfigService } from '@nestjs/config';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly configService: ConfigService,
) {}
@Post('signup')
async signup(@Body() signupData: SignupDto) {
return this.authService.signup(signupData);
}
@Post('signin')
@UseGuards(LocalAuthGuard)
async login(
@CurrentUser() user: User,
@Res({ passthrough: true }) response: Response,
) {
await this.authService.login(user, response);
}
@Get('google')
@UseGuards(GoogleAuthGuard)
loginGoogle() {}
@Get('google/callback')
@UseGuards(GoogleAuthGuard)
async googleCallback(
@CurrentUser() user: User,
@Res({ passthrough: true }) response: Response,
) {
await this.authService.login(user, response, true);
}
}
user.service.ts
import { InjectModel } from '@nestjs/mongoose';
import { User } from './schema/user.schema';
import { FilterQuery, Model, UpdateQuery } from 'mongoose';
import { hash } from 'bcryptjs';
import { SignupDto } from 'src/auth/dtos/signup.dto';
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name)
private readonly userModel: Model<User>,
) {}
async createUser(data: SignupDto) {
if(data.authProvider === 'local'){
data.password = await hash(data.password, 10);
}
const createdUser = new this.userModel(data);
await createdUser.save();
return createdUser;
}
async getUser(query: FilterQuery<User>) {
const user = await this.userModel.findOne(query);
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
async updateUser(query: FilterQuery<User>, data: UpdateQuery<User>) {
return this.userModel.findOneAndUpdate(query, data);
}
async getOrCreateUser(data: SignupDto) {
const user = await this.userModel.findOne({ email: data.email });
if (user && data.password === '') {
return user;
}
return this.createUser(data);
}
}
google.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-google-oauth20';
import { UsersService } from '../../users/users.service';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
constructor(
configService: ConfigService,
private readonly usersService: UsersService,
) {
super({
clientID: configService.getOrThrow('GOOGLE_AUTH_CLIENT_ID'),
clientSecret: configService.getOrThrow('GOOGLE_AUTH_CLIENT_SECRET'),
callbackURL: configService.getOrThrow('GOOGLE_AUTH_REDIRECT_URI'),
scope: ['profile', 'email'],
});
}
async validate(_accessToken: string, _refreshToken: string, profile: any) {
if (!profile.emails || !profile.emails[0]?.value) {
throw new UnauthorizedException('No email found in Google profile');
}
const email = profile.emails[0]?.value;
const existingUser = await this.usersService.getUser({ email });
if (existingUser && existingUser.authProvider === 'local') {
throw new UnauthorizedException(
'This email is already registered using password login.'
);
}
const user = await this.usersService.getOrCreateUser({
name: profile.displayName,
email: profile.emails[0]?.value,
password: '',
authProvider: 'google',
});
return user;
}
}
google-auth.guard.ts
// google-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class GoogleAuthGuard extends AuthGuard('google') {
handleRequest(err, user, info, context, status) {
const res = context.switchToHttp().getResponse();
if (err || !user) {
// Redirect to frontend with error message
return res.redirect(
`${process.env.FRONTEND_URL}/auth/signin?message=Authentication failed`
);
}
return user;
}
}