Está en la página 1de 33

Back End

Carrera
Programador
full-stack

AUTHENTICATION
CFS
AUTHENTICATION
La autenticación es parte esencial en la mayoría de las
aplicaciones. Hay varias estrategias que podemos emplear.

Aquí veremos una basada en token de usuario que consiste en


generar una clave encriptada -a través de un algoritmo- y
entregarla al cliente cuando se registra o se loguea en el sistema.

Luego, es responsabilidad del cliente mantener ese token y


enviarlo en las peticiones que requieran autorización.

Cada token es único y seguro. Nuestra API puede verificar


fácilmente que el token lleva su “firma” y que sigue vigente, pues
estos incluyen regularmente un tiempo de validez, vencido el cuál
ya no pueden ser usados para acceder a un recurso protegido.
CFS
Un token es una autorización que no puede ser manipulada. Es generada por el
servidor utilizando una clave secreta.
¿Cómo funciona?
1. El usuario envía una petición de login al servidor.
2. El servidor le entrega un token.
3. A partir de aquí, cada vez que el usuario quiere acceder a un recurso protegido,
envía el token junto la petición.
4. El servidor verifica la validez del token antes de entregar el recurso solicitado.
CFS
¿Cómo se ve un token?

Los tokens suelen incluir un payload en


el que transportan cierta información del
poseedor (bearer). No es buena práctica
incluir muchos datos y jamás deben
transportar información sensible.
CFS

VAMOS A CONCRETAR NUESTROS REQUISITOS

Para este caso de uso, los clientes comenzarán autenticándose


con un email y una contraseña.

Una vez autenticados, el servidor emitirá un JSON Web Token


que puede enviarse como bearer token en una cabecera de
autorización en solicitudes posteriores para demostrar la
autenticación: (“headers”: {“authorization”: [token]}) .

También crearemos una ruta protegida a la que sólo podrán


acceder las peticiones que contengan un JWT válido.
CFS

Creo que entendí…


¿Por dónde empiezo?
CFS
Empezaremos con el primer requisito: autenticar a un
usuario*.

1. Vamos a comenzar generando un modulo de autenticación


que contenga un servicio y un controlador.
2. El servicio va a implementar la lógica de autenticación.
3. El controlador expondrá los endpoints de autenticación.

nest g module auth


nest g controller auth
nest g service auth

*Luego lo ampliaremos emitiendo un JWT. Finalmente, crearemos una ruta protegida que compruebe si la
petición contiene un JWT válido.
CFS

A medida que implementemos el Servicio de autenticación


(AuthService), nos resultará útil encapsular las operaciones de
Usuario en un Servicio de Usuario (UserService).
Vamos a generar entonces un módulo y un servicio de usuario.
Agregaremos también una interfaz para tipar la entidad
“usuario”.

nest g module user


nest g service user
nest g interface user
CFS

Además de la interfaz, agregaremos un usuario al


archivo que utilizamos como base de datos. Esto nos
permitirá hacer login sin necesidad de implementar un
register para crear el primer usuario.

export interface User { "users": [


id: number; {
fullName: string; "id": 1,
"fullName": "Ricardo Grimes",
email: string;
"email": "rickgrimes@yahoo.com",
password: string;
"password": "1234abcd"
} }
]
CFS
Nuestro UserService debería quedar más o menos así:

import { Injectable, HttpStatus, HttpException } from '@nestjs/common';


import { User } from './user.interface';
const BASE_URL = 'http://localhost:3030/users/';

@Injectable()
export class UserService {
async findUser(email: string): Promise<User> {
const res = await fetch(BASE_URL);
if (!res.ok)
throw new HttpException(
'Internal Server Error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
const allUsers = await res.json();
const user = allUsers.find((usr: User) => usr.email === email);
return user;
}
}
CFS
El servicio que acabamos de crear se vincula con las entidades
“usuario”. Busca una de acuerdo a un email y, si la encuentra,
la retorna.

Pero el servicio será utilizado en el módulo de autenticación


(lo creamos en el módulo de usuario por una cuestión de
buenas prácticas).

Entonces, si lo vamos a usar en otro módulo, tenemos que


exportarlo de user.module.ts para hacerlo visible.

import { Module } from '@nestjs/common';


import { UserService } from './user.service';

@Module({
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
CFS
Login endpoint

Nuestro AuthService tiene la tarea de recuperar un usuario y verificar su


contraseña. Para ello creamos el método login().

import { Injectable, UnauthorizedException } from '@nestjs/common';


import { UserService } from 'src/user/user.service';

@Injectable()
export class AuthService {
constructor(private userService: UserService) {}
async login(email: string, pass: string): Promise<any> {
const user = await this.userService.findUser(email);
if (user?.password !== pass) {
throw new UnauthorizedException();
}
const { password, ...rest } = user;
return rest;
}
}
CFS
Login endpoint

Ahora actualizamos nuestro auth/auth.module.ts para importar


el módulo de usuario (UserModule)

import { Module } from '@nestjs/common';


import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserModule } from 'src/user/user.module';

@Module({
imports: [UserModule],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
CFS
Login endpoint
Con las piezas en su lugar, solo resta abrir AuthController y agregar un método login,
que va a ser llamado por el cliente para autenticar un usuario.

Recibirá el email y la contraseña en el cuerpo de la petición y retornará un token JWT si


el usuario es autenticado por el servicio. Aunque por ahora, solo deseamos que retorne
un usuario o un error de autenticación.

No olvidemos el DTO, ahora que ya sabemos usarlos para validar las peticiones

auth/login.dto.ts
import { IsEmail, IsString, MaxLength, MinLength } from
'class-validator';
export class LoginDto {
@IsEmail()
email: string;
@MinLength(6)
@MaxLength(15)
@IsString()
password: string;
}
CFS
Login endpoint
Y por último, nuestro controlador:

auth/auth.controller.ts

import { Controller, HttpCode, HttpStatus, Post, Body } from


'@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './login.dto';

@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto.email, loginDto.password);
}
}
CFS

Con email y/o contraseña incorrectos


CFS

Con email y contraseña correctos


CFS
JWT Token
Estamos listos para pasar a la parte JWT de nuestro sistema de autenticación.
Vamos a revisar y refinar nuestros requisitos:

• Permitir a los usuarios autenticarse con email y contraseña,


devolviendo un JWT para su uso en llamadas posteriores a
endpoints de API protegidos. Estamos bien encaminados para
cumplir este requisito. Para completarlo, necesitaremos escribir el
código que emite un JWT.

• Crear rutas API protegidas en base a la presencia de un JWT válido


como bearer token.*

Necesitaremos instalar un paquete adicional para soportar JWT.

npm install
@nestjs/jwt

* No hay traducción literal. En este contexto significa “vale al portador”, es decir, un objeto que
trae el portador y le garantiza acceso a un recurso.
CFS
JWT Token
Vamos a mantener nuestros servicios modularizados y
limpios, para lo cual nos conviene generar el token JWT
en AuthService.

1. Inyectamos JwtService (del paquete que


acabamos de instalar)

2. Actualizamos el método login() para que


genere el token
CFS
JWT Token
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserService } from 'src/user/user.service';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
) {}

async login(email: string, pass: string): Promise<any> {


const user = await this.userService.findUser(email);
if (user?.password !== pass) {
throw new UnauthorizedException();
}
const payload = { sub: user.id, name: user.fullName, email: user.email };
return {
access_token: await this.jwtService.signAsync(payload),
};
}
}
CFS
JWT Token

¿QUÉ ACABAMOS DE HACER?

Estamos usando la librería @nestjs/jwt, que prove una función signAsync() para
generar nuestros JWT a partir de un subconjunto de propiedades del objeto user (el
payload).

Luego retornamos ese JWT como un objeto con el nombre access_token.


El nombre de este objeto es convencional, puede llamarse de cualquier modo.

Asimismo, la propiedad sub representa el id del usuario. Le ponemos sub para ser
consistentes con el estándar que propone JWT, pero también puede llamarse con
cualquier nombre… aunque la sabiduría popular recomienda:

Allí donde fueres, haz lo que vieres


CFS
JWT Token
Ahora necesitamos modificar AuthModule para importar las nuevas
dependencias y configurar el JwtModule. Antes, vamos a crear la clave
secreta que usaremos para generar tokens y para verificar la validez de los
tokens que entren en las peticiones.

auth/constants.ts

export const jwtConstants = {


secret: 'Super_$_S3cr3t_#_Key_@_',
};

/*
Esto es solo para fines educativos. La clave no debe estar
expuesta. Debe ir fuera del código. ¿Por qué?.
Porque con ella se pueden crear y verificar tokens.
Es habitual almacenarla en variables de entorno o en una
base de datos segura.
*/
CFS auth/auth.module.ts

import { Module } from '@nestjs/common';


import { AuthService } from './auth.service';
import { UserModule } from 'src/user/user.module';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { jwtConstants } from './constants';

@Module({
imports: [
UserModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthService],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
CFS
Ahora, si hacemos una petición de tipo POST al endpoint /auth/login y el par
email / contraseña coincide con los datos almacenados en un registro de
nuestra base de datos, el servidor nos entrega en su respuesta un JWT.

A partir de aquí le corresponde al cliente guardar su token para enviarlo


en subsiguientes peticiones que lo requieran.
CFS
Rutas Protegidas
Aunque les llamamos así, lo que en verdad estamos protegiendo
son los recursos a los que se accede a través de esas rutas.

Supongamos que para borrar un recurso de tipo track hay que


estar autenticado. Entonces, podemos poner un “guardia de
autenticación” en ese endpoint. Este verificará cada request de
tipo DELETE para determinar dos cosas:

1. Si tiene un token firmado por el servidor.


2. Si el token no expiró.

Solo entonces permitirá acceso al recurso.


CFS

Authentication Guard

Ahora podemos encargarnos del requerimiento


final: proteger nuestros endpoints pidiendo un
JWT válido en las requests.

Crearemos un AuthGuard que protegerá


nuestras rutas / recursos.
CFS auth/auth.guard.ts

import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}

/* El siguiente método es la parte central del guardia de


autenticación. AuthGuard implementa la interfaz CanActivate de NestJS.
Esta función es llamada automáticamente por NestJS antes de permitir el
acceso a una ruta protegida. */
CFS auth/auth.guard.ts

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: jwtConstants.secret,
});
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
CFS auth/auth.guard.ts

/* Este es un método privado utilizado por canActivate para extraer


el token de autorización del encabezado "Authorization" de la solicitud
HTTP. Si el encabezado de autorización no está presente o no contiene
el formato adecuado, este método devuelve undefined.*/

private extractTokenFromHeader(request: Request): string | undefined


{
const [type, token] = request.headers.authorization?.split(' ') ??
[];
return type === 'Bearer' ? token : undefined;
}
}

Ahora ya podemos registrar nuestro AuthGuard para


proteger las rutas que sea necesario. Nosotros vamos a
hacerlo en TrackController.
CFS
En /track/track.controller.ts agregaremos el decorador
UseGuards a la lista de decoradores importados previamente:

import {Get, Post, Delete, Put, Controller, Param, Body,


HttpCode, HttpStatus, ParseIntPipe, UseGuards} from
'@nestjs/common';

Importamos el guardián de ruta propiamente dicho:

import { AuthGuard } from 'src/auth/auth.guard';

Incorporamos el guardia de ruta AuthGuard con el decorador


@UseGuards en la ruta DELETE

@UseGuards(AuthGuard)
@Delete(':id')
CFS

Si lanzamos una request contra esa ruta y no


tenemos token o el token no es válido, no
podremos acceder.
Es más, el controlador ni siquiera ejecutará.
CFS

Pero si la request efectivamente lleva en el


encabezado “Authorization” un JWT válido (y
por válido entendemos que haya sido firmado
por el backend y que no esté vencido)…
CFS

Y así es como nuestra API


ahora puede proteger
cualquier recurso
con solo agregar el
AuthGuard a la ruta
apropiada.

También podría gustarte