Skip to content

Instantly share code, notes, and snippets.

@WanerValencia
Created June 20, 2023 02:28
Show Gist options
  • Save WanerValencia/2d5cba388cee98677891bb04029abbfa to your computer and use it in GitHub Desktop.
Save WanerValencia/2d5cba388cee98677891bb04029abbfa to your computer and use it in GitHub Desktop.
Autenticación Google y Passport en Nestjs

Inicio de sesión con Google en NestJS

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.

Paquetes necesarios

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

Código

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.

Configuración del Módulo de Autenticació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.

Controlador de Autenticación de 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,
    };
  }
}

Servicio de Autenticación de Google

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment