Últimamente, he estado pensando en cómo hacer que algunos de mis puntos finales cruciales sean más seguros al utilizar JWT almacenado localmente.
Puede que no sea la mejor práctica para la seguridad (debido a la posibilidad de ataques XSS), pero no era un requisito que yo estableciera. Tuve que adaptarme.
Para que esto sea más seguro, encontré una solución que espero que también te ayude.
Supongo que todos sabemos qué es JWT: un token emitido por el servicio backend, que no puede ser modificado por el frontend, porque eso cambiaría la firma e invalidaría automáticamente el token.
Suena genial, hasta que nos encontramos con una situación en la que el token puede ser robado, como en mi caso con el ataque XSS. Muchos sitios web almacenan tokens en el almacenamiento local en lugar de en cookies exclusivas de HTTP, por lo que también son vulnerables.
Entonces, cuando no es posible cambiar la forma en que el frontend almacena ese token, tenemos que cambiar la forma en que emitimos y validamos el token, y este es el momento en el que quiero presentarles una solución simple: JWT con huella digital hash, pero veamos el código.
Supongamos que tenemos un punto final para emitir tokens, llamémoslo punto final de inicio de sesión.
@Controller('v1/sign-in') export class SignInAction { constructor(private jwtService: JwtService) {} @Post() async handle( @Req() request: Request, @Body() body: SignInHttpRequest, ): Promise<SignInHttpResponse> { const ip = request.headers['x-forwarded-for'] || request.socket.remoteAddress; const userAgent = request.headers['user-agent']; const token = this.jwtService.sign({ email: body.email, fingerprint: getSHA512Hash(`${ip}${userAgent}`), // It's worth to mention, that to make it even more secure, you should add salt }); return { token, }; } }
Entonces, lo que estamos haciendo aquí es básicamente recolectar el agente de usuario y la IP remota del usuario que se está conectando, codificarlo y luego colocarlo en el token JWT, por lo que incluso si el usuario o el atacante quisieran ver qué hay dentro del token JWT, solo verán un valor codificado aleatorio.
Pero, ¿cómo va a mejorar mi seguridad? Echemos un vistazo a nuestro segundo punto final, mostremos la huella digital.
@Controller('v1/fingerprint') @UseGuards(AuthGuard(JWT_STRATEGY)) export class ShowFingerPrintAction { @Get() async handle(@Req() request) { return { fingerprint: request.user.fingerprint, }; } }
En un principio, no es nada especial. Punto final que devuelve la huella digital de la solicitud, ¿qué lo hace más seguro? ¡JWT_STRATEGY!
export const JWT_STRATEGY = 'JWT'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: 'hard!to-guess_secret', passReqToCallback: true, }); } async validate(request: Request, payload: any): Promise<any> { const fingerprint = payload.fingerprint; const ip = request.headers['x-forwarded-for'] || request.socket.remoteAddress; const userAgent = request.headers['user-agent']; const calculatedFingerprint = getSHA512Hash(`${ip}${userAgent}`); // It's worth to mention, that to make it even more secure, you should add salt if (fingerprint !== calculatedFingerprint) { throw new BadRequestException('Invalid fingerprint'); } return payload; } }
Como puede ver, en la estrategia, extraemos los mismos valores que cuando los extrajimos del punto final de inicio de sesión y los comparamos: si la IP o el agente de usuario han cambiado, denegaremos el acceso a nuestro punto final de huella digital.
Este es el caso en el que incluso si el atacante robara nuestro token, no podría hacer nada con él, porque
Así es como podemos proteger nuestro punto final crucial de una manera muy sencilla, aunque, por supuesto, hay algunas desventajas.
Para el primer problema, por supuesto, podrías verificar solo la IP y ni siquiera hacer un hash al emitir un token, pero entonces el atacante potencial conoce el vector del ataque (suplantación de IP u otros métodos), por lo que es más seguro sacrificar algo de rendimiento por seguridad.
Espero que este método le ayude a introducir una capa adicional de seguridad en sus puntos finales cruciales.
Enlace al código fuente funcional: Código fuente
Consejos: