Cómo crear una API REST segura de Nest.js utilizando JWT y MongoDB
Utilice la arquitectura estructurada de Nest para crear API REST seguras y eficientes.
Express.js es una excelente tecnología para crear API REST seguras y sólidas; sin embargo, no proporciona una estructura predefinida. Su naturaleza minimalista le permite manejar aspectos esenciales como enrutamiento, organización del código y medidas de seguridad, ya sea manualmente o aprovechando el middleware y las bibliotecas disponibles.
Por el contrario, Nest.js, construido sobre Express.js y Node.js, introduce una abstracción de nivel superior que ofrece una estructura clara, un enfoque de organización de código sólido y detalles de implementación simplificados. Básicamente, Nest.js proporciona una arquitectura más estructurada para crear servicios y API de backend eficientes y seguros.
Configurar un proyecto Nest.js
Para comenzar, primero debe instalar la línea de comando (CLI) de Nest.js globalmente ejecutando el siguiente comando:
npm i -g @nestjs/cli
Una vez que se complete la instalación, continúe y cree un nuevo proyecto ejecutando:
nest new nest-jwt-api
A continuación, la CLI de Nest.js le pedirá que elija un administrador de paquetes para instalar las dependencias. Para este tutorial, usaremos npm, el administrador de paquetes de nodos. Seleccione npm y espere mientras la CLI crea un proyecto Nest.js básico e instala todos los archivos de configuración necesarios y las dependencias iniciales necesarias para ejecutar la aplicación.
Una vez configurado el proyecto, navegue hasta el directorio del proyecto e inicie el servidor de desarrollo.
cd nest-jwt-api
npm run start
Finalmente, ejecute el siguiente comando para instalar los paquetes que usaremos para este proyecto.
npm install mongodb mongoose @nestjs/mongoose @types/bcrypt bcrypt jsonwebtoken @nestjs/jwt
Puede encontrar el código de este proyecto en este repositorio de GitHub.
Configurar la conexión de la base de datos MongoDB
Configure una base de datos MongoDB localmente o configure un clúster de MongoDB en la nube. Después de configurar la base de datos, copie la cadena URI de conexión de la base de datos, cree un archivo .env en el directorio raíz de nuestra carpeta de proyecto y pegue la cadena de conexión:
MONGO_URI="connection string"
A continuación, actualice app.module.ts en el archivo del directorio src para configurar Mongoose de la siguiente manera:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserAuthModule } from './user-auth/user-auth.module';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
}),
MongooseModule.forRoot(process.env.MONGO_URI),
UserAuthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
El código proporcionado configura tres módulos esenciales para la aplicación Nest.js: ConfigModule para la configuración del entorno, MongooseModule para establecer la conexión MongoDB y UserAuthModule para autenticacion de usuario. Tenga en cuenta que, en esta etapa, puede ocurrir un error ya que el UserAuthModule aún no está definido, pero lo crearemos en la siguiente sección.
Creación del módulo de autenticación de usuario
Para mantener un código limpio y bien organizado, cree un módulo de autenticación de usuario ejecutando el siguiente comando.
nest g module user-auth
La herramienta CLI de Nest.js genera automáticamente los archivos del módulo necesarios. Además, actualizará el archivo app.module.ts, incorporando los cambios necesarios relacionados con el módulo de autenticación de usuarios.
Puede optar por crear los archivos de configuración principales del proyecto manualmente; sin embargo, la herramienta CLI simplifica este proceso al crear automáticamente los elementos necesarios, además de actualizar los cambios en consecuencia en el archivo app.module.ts. .
Crear un esquema de usuario
Dentro de la carpeta user-auth recién creada en el directorio src, cree un nuevo archivo schemas/user-auth.schema.ts y agregue el siguiente código para crear un esquema Mongoose para el modelo Usuario
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
@Schema({ timestamps: true })
export class User {
@Prop()
username: string;
@Prop()
password: string;
}
export type UserDocument = User & Document;
export const UserSchema = SchemaFactory.createForClass(User);
Crear el servicio de autenticación de usuario
Ahora, creemos el servicio de autenticación de usuarios que administrará la lógica de autenticación para la API REST ejecutando el siguiente comando:
nest g service user-auth
Este comando creará un archivo user-auth.service.ts dentro del directorio user-auth. Abra este archivo y actualícelo con el siguiente código.
Primero, realice las siguientes importaciones.
import { Injectable, NotFoundException, Logger, UnauthorizedException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { User } from './schemas/user-auth.schema'; import * as bcrypt from 'bcrypt'; import { JwtService } from '@nestjs/jwt';
- Luego, cree una clase UserAuthService que encapsule la funcionalidad para el registro de usuarios, el inicio de sesión y la recuperación de todas las rutas de datos del usuario.
@Injectable()
export class UserAuthService {
private readonly logger = new Logger(UserAuthService.name);
constructor( @InjectModel(User.name) private userModel: Model<User>, private jwtService: JwtService) {}
async registerUser(username: string, password: string): Promise<{ message: string }> {
try {
const hash = await bcrypt.hash(password, 10);
await this.userModel.create({ username, password: hash });
return { message: 'User registered successfully' };
} catch (error) {
throw new Error('An error occurred while registering the user');
}
}
async loginUser(username: string, password: string): Promise<string> {
try {
const user = await this.userModel.findOne({ username });
if (!user) {
throw new NotFoundException('User not found');
}
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
throw new UnauthorizedException('Invalid login credentials');
}
const payload = { userId: user._id };
const token = this.jwtService.sign(payload);
return token;
} catch (error) {
console.log(error);
throw new UnauthorizedException('An error occurred while logging in');
}
}
async getUsers(): Promise<User[]> {
try {
const users = await this.userModel.find({});
return users;
} catch (error) {
this.logger.error(`An error occurred while retrieving users: ${error.message}`);
throw new Error('An error occurred while retrieving users');
}
}
}
La clase UserAuthService implementa la lógica de registro de usuario, inicio de sesión y recuperación de datos del usuario. Utiliza el userModel para interactuar con la base de datos y realizar las acciones requeridas, incluido el hash de la contraseña durante el registro, la validación de las credenciales de inicio de sesión y, por último, la generación de tokens JWT después de una autenticación exitosa.
Implementación de la protección de autenticación
Para garantizar la seguridad de los recursos confidenciales, es fundamental limitar el acceso exclusivamente a usuarios autorizados. Esto se logra aplicando una medida de seguridad que exige la presencia de un JWT válido en solicitudes API posteriores realizadas a puntos finales protegidos, en este caso, la ruta usuarios. En el directorio user-auth, cree un nuevo archivo auth.guard.ts y agregue el código siguiente.
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { secretKey } from './config';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: secretKey.secret,
});
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
El código implementa una protección, como se especifica en la documentación oficial, para proteger las rutas y garantizar que solo los usuarios autenticados con un token JWT válido puedan acceder a ellas.
Extrae el token JWT del encabezado de la solicitud, verifica su autenticidad utilizando JwtService y asigna la carga útil decodificada a la propiedad request['user'] para su posterior procesamiento. Si el token falta o no es válido, genera una Excepción no autorizada para impedir el acceso a la ruta protegida.
Ahora, cree el archivo config.ts en el mismo directorio y agregue el código a continuación.
export const secretKey = {
secret: 'SECTRET VALUE.',
};
Esta clave secreta se utiliza para firmar y verificar la autenticidad de los JWT. Es esencial almacenar el valor clave de forma segura para evitar el acceso no autorizado y proteger la integridad de los JWT.
Definir el controlador API
Cree un controlador que maneje los puntos finales de API para la autenticación de usuarios.
nest g controller user-auth
A continuación, copie el código proporcionado en este archivo del repositorio de GitHub y agréguelo al archivo user-auth.controller.ts: define los puntos finales para el registro de usuarios, el inicio de sesión y la recuperación de datos del usuario. El decorador UseGuards(AuthGuard) se incluye para aplicar la autenticación para el punto final getUsers, lo que garantiza que solo se conceda acceso a los usuarios autenticados.
Actualice el archivo user-auth.module.ts
Para reflejar los cambios realizados en el proyecto, actualice el archivo user-auth.module.ts para configurar los módulos, servicios y controladores necesarios para la autenticación de usuarios.
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { UserAuthController } from './user-auth.controller';
import { UserAuthService } from './user-auth.service';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from './schemas/user-auth.schema';
import { secretKey } from './config';
@Module({
imports: [
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
JwtModule.register({
secret: secretKey.secret,
signOptions: { expiresIn: '1h' },
}),
],
controllers: [UserAuthController],
providers: [UserAuthService],
})
export class UserAuthModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
}
}
Finalmente, active el servidor de desarrollo y pruebe los puntos finales de la API utilizando Postman.
npm run start
Creación de API REST seguras de Nest.js
La creación de API REST de Nest.js seguras requiere un enfoque integral que va más allá de simplemente depender de JWT para la autenticación y autorización. Si bien los JWT son importantes, es igualmente crucial implementar medidas de seguridad adicionales.
Además, al priorizar la seguridad en cada etapa del desarrollo de API, puede garantizar la seguridad de sus sistemas backend.