La autenticación con Google se realiza a través de una estrategia de Passport llamada GoogleStrategy
, que se configura utilizando un ID de cliente de Google, un secreto de cliente y una URL de devolución de llamada.
Para lograr la autenticación con Google, necesitamos instalar las siguientes dependencias en nuestro proyecto:
NPM:
npm install --save passport-google-oauth20
Yarn:
yarn add passport-google-oauth20
Comenzaremos con la estrategia GoogleStrategy
:
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IGoogleUser } from '../auth.interfaces';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(configService: ConfigService) {
super({
clientID: configService.getOrThrow('GOOGLE_CLIENT_ID'),
clientSecret: configService.getOrThrow('GOOGLE_CLIENT_SECRET'),
callbackURL: configService.getOrThrow('GOOGLE_CALLBACK_URL'),
scope: configService.getOrThrow<string>('GOOGLE_SCOPES').split(','),
});
}
authorizationParams(options: any): object {
return {
...options,
access_type: 'offline',
prompt: 'consent',
};
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { name, emails, photos } = profile;
const user: IGoogleUser = {
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
picture: photos[0].value,
accessToken,
refreshToken,
};
done(null, user);
}
}
En este código, estamos definiendo una estrategia de Google Passport que toma los valores de configuración de las variables de entorno y los usa para autenticar a los usuarios con Google.
La función validate()
recibe la información del perfil de Google del usuario, que contiene el nombre, correo electrónico, foto de perfil y los tokens de acceso y actualización. A partir de esta información, creamos un objeto user
que luego pasa al método done()
de Passport, que se encarga de generar el usuario y de la sesión.
El AuthModule
es donde agrupamos todas nuestras estrategias de autenticación y lo importamos en nuestro módulo principal:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GoogleStrategy } from './strategies/google.strategy';
import { GoogleAuthService } from './services/google.auth.service';
import { UsersModule } from 'src/users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './controllers/auth.controller';
import { GoogleAuthController } from './controllers/google.auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [
ConfigModule.forRoot(),
UsersModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWTKEY,
signOptions: { expiresIn: process.env.TOKEN_EXPI
RATION },
}),
],
controllers: [AuthController, GoogleAuthController],
providers: [GoogleAuthService, GoogleStrategy, JwtStrategy],
})
export class AuthModule {}
Aquí, hemos importado y proporcionado GoogleStrategy
y GoogleAuthService
, que se encargan de la lógica de autenticación con Google.
El controlador GoogleAuthController
se encarga de manejar las rutas relacionadas con la autenticación de Google.
El método googleAuth
inicia el flujo de autenticación, y googleAuthRedirect
se encarga de recibir la respuesta de Google tras la autenticación.
La función googleRefresh
maneja la renovación del token de Google cuando este expira, y googleCheck
valida el token de Google:
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request, Response } from 'express';
import { ConfigService } from '@nestjs/config';
import { AuthProviders } from '../../core/enums';
import { GoogleAuthService } from '../services/google.auth.service';
@Controller({
path: 'auth/google',
})
export class GoogleAuthController {
constructor(
private readonly googleAuthService: GoogleAuthService,
private readonly configService: ConfigService,
) {}
@Get('')
@UseGuards(AuthGuard('google'))
async googleAuth(@Req() req) {}
@Get('redirect')
@UseGuards(AuthGuard('google'))
async googleAuthRedirect(@Req() req, @Res() res: Response) {
const frontendBaseRedirectURL = this.configService.get<string>(
'FRONTEND_AUTH_REDIRECT_URL',
);
if (!req.user) return res.redirect(301, frontendBaseRedirectURL);
let frontendUrl = frontendBaseRedirectURL;
try {
const { token, googleRefreshToken } = await this.googleAuthService.login(
req.user,
);
frontendUrl += `?provider=${AuthProviders.GOOGLE}&authToken=${token}&googleRefreshToken=${googleRefreshToken}`;
} catch (error) {
frontendUrl += `?provider=${AuthProviders.GOOGLE}&error=${error.message}`;
}
return res.redirect(301, frontendUrl);
}
@Get('refresh')
async googleRefresh(@Req() req: Request) {
const authorization = req.headers.authorization;
const newToken = await this.googleAuthService.refreshGoogleToken(
authorization,
);
return {
message: 'Refreshed Google token',
headers: {
Authorization: newToken,
},
};
}
@Get('check')
async googleCheck(@Req() req: Request) {
const authorization = req.headers.authorization;
const email = await this.googleAuthService.checkGoogleToken(authorization);
return {
message: 'Valid Google token',
email,
};
}
}
El servicio GoogleAuthService
contiene la lógica principal para la autenticación de Google, incluyendo el inicio de sesión, la renovación y validación del token:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { IGoogleUser, IJwtPayload } from '../auth.interfaces';
import { UsersService } from 'src/users/users.service';
import { AuthProviders } from 'src/core/enums';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { google } from 'googleapis';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class GoogleAuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
async login(user: IGoogleUser) {
const { email, firstName, lastName, picture, accessToken, refreshToken } =
user;
let userEntity = await this.usersService.find({
where: { email },
});
if (userEntity && userEntity?.provider !== AuthProviders.GOOGLE) {
throw new Error('Email already exists');
}
if (!userEntity) {
const newUser = {
email,
first_name: firstName,
last_name: lastName,
image_url: picture,
provider: AuthProviders.GOOGLE,
};
userEntity = await this.usersService.create(newUser);
}
const payload: IJwtPayload = {
id: userEntity.id,
googleAccessToken: accessToken,
googleRefreshToken: refreshToken,
};
const token = await this.generateToken(payload);
return {
message: 'User information from Google',
user,
token,
googleRefreshToken: refreshToken,
};
}
async checkGoogleToken(token: string) {
const oAuth2Client = new google.auth.OAuth2(
this.configService.getOrThrow('GOOGLE_CLIENT_ID'),
this.configService.getOrThrow('GOOGLE_CLIENT_SECRET'),
);
try {
const result = await oAuth2Client.getTokenInfo(token);
const invalidToken = new Date().getTime() > result.expiry_date;
if (invalidToken) {
throw new UnauthorizedException({
message: 'Invalid Google token',
});
}
return result.email;
} catch (error) {
throw new UnauthorizedException({
message: 'Invalid Google token',
});
}
}
async refreshGoogleToken(refreshToken: string) {
const oAuth2Client = new google.auth.OAuth2(
this.configService.getOrThrow('GOOGLE_CLIENT_ID'),
this.configService.getOrThrow('GOOGLE_CLIENT_SECRET'),
);
try {
oAuth2Client.setCredentials({
refresh_token: refreshToken,
});
const {
credentials: { access_token, refresh_token },
} = await oAuth2Client.refreshAccessToken();
const { email } = await oAuth2Client.getTokenInfo(access_token);
const userEntity = await this.usersService.find({
where: { email },
});
const payload: IJwtPayload = {
id: userEntity.id,
googleAccessToken: access_token,
googleRefreshToken: refresh_token,
};
const token = await this.generateToken(payload);
return token;
} catch (error) {
throw new UnauthorizedException({
message: 'Invalid Google Refresh Token',
});
}
}
private async generateToken(payload: IJwtPayload, options?: JwtSignOptions) {
const token = await this.jwtService.signAsync(
{
...payload,
},
options,
);
return token;
}
}
Esto debería proporcionar un buen punto de partida para implementar la autenticación con Google en una aplicación NestJS. Recuerda configurar las variables de entorno (GOOGLE_CLIENT_ID
, GOOGLE_CLIENT_SECRET
, GOOGLE_CALLBACK_URL
, GOOGLE_SCOPES
) con tus propios valores de configuración de Google OAuth.