From a764e1d44104a3c0f68c63315ee9157606c69f17 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 19 Feb 2025 21:23:53 +0000 Subject: [PATCH] Fixed authentication. --- .../src/auth/auth.access.service.ts | 8 +- .../src/auth/auth.controller.ts | 118 +++++++++++------- .../nestjs-seshat-api/src/auth/auth.module.ts | 4 +- .../src/auth/auth.refresh.service.ts | 50 +++++--- .../src/auth/auth.service.ts | 18 +-- .../entities/auth.refresh-token.entity.ts | 6 +- .../src/auth/guards/jwt-access.admin.guard.ts | 11 +- .../src/auth/guards/jwt-access.guard.ts | 11 +- .../auth/strategies/jwt-refresh.strategy.ts | 39 +++++- .../src/auth/strategies/jwt.strategy.ts | 32 ++++- .../src/logging.serializers.ts | 29 ++++- 11 files changed, 218 insertions(+), 108 deletions(-) diff --git a/backend/nestjs-seshat-api/src/auth/auth.access.service.ts b/backend/nestjs-seshat-api/src/auth/auth.access.service.ts index 83892b1..ba9729f 100644 --- a/backend/nestjs-seshat-api/src/auth/auth.access.service.ts +++ b/backend/nestjs-seshat-api/src/auth/auth.access.service.ts @@ -1,7 +1,7 @@ import * as moment from 'moment'; import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { UserEntity } from 'src/users/users.entity'; +import { UserEntity } from 'src/users/entities/users.entity'; import { ConfigService } from '@nestjs/config'; import { PinoLogger } from 'nestjs-pino'; @@ -22,9 +22,9 @@ export class AuthAccessService { { username: user.userLogin, sub: user.userId, - iat: now.getTime(), - nbf: now.getTime(), - exp: expiration.getTime(), + iat: Math.floor(now.getTime() / 1000), + nbf: Math.floor(now.getTime() / 1000) - 5 * 60, + exp: Math.floor(expiration.getTime() / 1000), }, { secret: this.config.getOrThrow('AUTH_JWT_ACCESS_TOKEN_SECRET'), diff --git a/backend/nestjs-seshat-api/src/auth/auth.controller.ts b/backend/nestjs-seshat-api/src/auth/auth.controller.ts index c7d90a2..d08ed6e 100644 --- a/backend/nestjs-seshat-api/src/auth/auth.controller.ts +++ b/backend/nestjs-seshat-api/src/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Request, Post, UseGuards, Body, Res } from '@nestjs/common'; +import { Controller, Request, Post, UseGuards, Body, Res, Delete, Patch } from '@nestjs/common'; import { LoginAuthGuard } from './guards/login-auth.guard'; import { AuthService } from './auth.service'; import { UsersService } from 'src/users/users.service'; @@ -6,7 +6,7 @@ import { RegisterUserDto } from './dto/register-user.dto'; import { Response } from 'express'; import { JwtRefreshGuard } from './guards/jwt-refresh.guard'; import { OfflineGuard } from './guards/offline.guard'; -import { UserEntity } from 'src/users/users.entity'; +import { UserEntity } from 'src/users/entities/users.entity'; import { QueryFailedError } from 'typeorm'; import { PinoLogger } from 'nestjs-pino'; import { JwtAccessGuard } from './guards/jwt-access.guard'; @@ -29,6 +29,7 @@ export class AuthController { try { data = await this.auth.login(req.user); if (!data.access_token || !data.refresh_token || !data.refresh_exp) { + response.statusCode = 500; return { success: false, error_message: 'Something went wrong with tokens while logging in.', @@ -42,6 +43,8 @@ export class AuthController { msg: 'Failed to login.', error: err, }); + + response.statusCode = 500; return { success: false, error_message: 'Something went wrong while logging in.', @@ -52,12 +55,14 @@ export class AuthController { httpOnly: true, secure: true, expires: new Date(data.exp), + sameSite: 'strict', }); response.cookie('Refresh', data.refresh_token, { httpOnly: true, secure: true, expires: new Date(data.refresh_exp), + sameSite: 'strict', }); this.logger.info({ @@ -75,17 +80,32 @@ export class AuthController { } @UseGuards(JwtAccessGuard) - @Post('logout') + @Delete('login') async logout( @Request() req, @Res({ passthrough: true }) response: Response, ) { - console.log('logout cookie', req.cookies?.Refresh); - // TODO: delete refresh token from database. - // await this.auth.delete(req.cookies?.Refresh); + const accessToken = req.cookies?.Authentication; + const refreshToken = req.cookies?.Refresh; - response.clearCookie('Refresh'); response.clearCookie('Authentication'); + response.clearCookie('Refresh'); + + if (!refreshToken || !await this.auth.revoke(req.user.userId, refreshToken)) { + // User has already logged off. + this.logger.info({ + class: AuthController.name, + method: this.login.name, + user: req.user, + msg: 'User has already logged off via ' + (!refreshToken ? 'cookies' : 'database'), + }); + + response.statusCode = 400; + return { + success: false, + error_message: 'User has already logged off.' + }; + } this.logger.info({ class: AuthController.name, @@ -94,61 +114,59 @@ export class AuthController { msg: 'User logged off', }); - return req.logout(); + return { + success: true, + }; } @UseGuards(JwtRefreshGuard) - @Post('refresh') + @Patch('login') async refresh( @Request() req, @Res({ passthrough: true }) response: Response, ) { - try { - const refresh_token = req.cookies.Refresh; - const data = await this.auth.renew(req.user, refresh_token); + this.logger.info({ + class: AuthController.name, + method: this.login.name, + user: req.user, + refresh_token: req.cookies.Refresh, + msg: 'User logged in.', + }); - response.cookie('Authentication', data.access_token, { + const refreshToken = req.cookies.Refresh; + const data = await this.auth.renew(req.user, refreshToken); + + response.cookie('Authentication', data.access_token, { + httpOnly: true, + secure: true, + expires: new Date(data.exp), + sameSite: 'strict', + }); + this.logger.debug({ + class: AuthController.name, + method: this.refresh.name, + user: req.user, + access_token: data.access_token, + msg: 'Updated Authentication cookie for access token.', + }); + + if (data.refresh_token != refreshToken) { + response.cookie('Refresh', data.refresh_token, { httpOnly: true, secure: true, - expires: new Date(data.exp), + expires: new Date(data.refresh_exp), + sameSite: 'strict', }); this.logger.debug({ class: AuthController.name, method: this.refresh.name, user: req.user, - access_token: data.access_token, - msg: 'Updated Authentication cookie for access token.', + refresh_token: data.refresh_token, + msg: 'Updated Refresh cookie for refresh token.', }); - - if (data.refresh_token != refresh_token) { - response.cookie('Refresh', data.refresh_token, { - httpOnly: true, - secure: true, - expires: new Date(data.refresh_exp), - }); - this.logger.debug({ - class: AuthController.name, - method: this.refresh.name, - user: req.user, - refresh_token: data.refresh_token, - msg: 'Updated Refresh cookie for refresh token.', - }); - } - - return { success: true }; - } catch (err) { - this.logger.error({ - class: AuthController.name, - method: this.refresh.name, - user: req.user, - msg: 'Failed to refresh tokens.', - error: err, - }); - return { - success: false, - error_message: 'Something went wrong.', - }; } + + return { success: true }; } @UseGuards(OfflineGuard) @@ -178,6 +196,8 @@ export class AuthController { user: req.user, msg: 'Failed to register due to duplicate userLogin.', }); + + response.statusCode = 400; return { success: false, error_message: 'Username already exist.', @@ -191,6 +211,8 @@ export class AuthController { msg: 'Failed to register.', error: err, }); + + response.statusCode = 500; return { success: false, error_message: 'Something went wrong when creating user.', @@ -208,6 +230,8 @@ export class AuthController { refresh_token: data.refresh_token, msg: 'Failed to generate tokens after registering.', }); + + response.statusCode = 500; return { success: false, error_message: 'Something went wrong with tokens while logging in.', @@ -221,6 +245,8 @@ export class AuthController { msg: 'Failed to login after registering.', error: err, }); + + response.statusCode = 500; return { success: false, error_message: 'Something went wrong while logging in.', @@ -231,12 +257,14 @@ export class AuthController { httpOnly: true, secure: true, expires: new Date(data.exp), + sameSite: 'strict', }); response.cookie('Refresh', data.refresh_token, { httpOnly: true, secure: true, expires: new Date(data.refresh_exp), + sameSite: 'strict', }); return { diff --git a/backend/nestjs-seshat-api/src/auth/auth.module.ts b/backend/nestjs-seshat-api/src/auth/auth.module.ts index b8ed274..385a4fc 100644 --- a/backend/nestjs-seshat-api/src/auth/auth.module.ts +++ b/backend/nestjs-seshat-api/src/auth/auth.module.ts @@ -12,13 +12,14 @@ import { AuthRefreshService } from './auth.refresh.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthRefreshTokenEntity } from './entities/auth.refresh-token.entity'; import { AuthAccessService } from './auth.access.service'; +import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy'; @Module({ imports: [ TypeOrmModule.forFeature([AuthRefreshTokenEntity]), ConfigModule, UsersModule, - PassportModule, + PassportModule.register({ session: false }), JwtModule.registerAsync({ imports: [ConfigModule], extraProviders: [ConfigService], @@ -35,6 +36,7 @@ import { AuthAccessService } from './auth.access.service'; AuthRefreshService, AuthService, JwtStrategy, + JwtRefreshStrategy, LoginStrategy, ], controllers: [AuthController] diff --git a/backend/nestjs-seshat-api/src/auth/auth.refresh.service.ts b/backend/nestjs-seshat-api/src/auth/auth.refresh.service.ts index c1d8468..3802dfc 100644 --- a/backend/nestjs-seshat-api/src/auth/auth.refresh.service.ts +++ b/backend/nestjs-seshat-api/src/auth/auth.refresh.service.ts @@ -5,7 +5,7 @@ import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; import { UUID } from 'crypto'; -import { UserEntity } from 'src/users/users.entity'; +import { UserEntity } from 'src/users/entities/users.entity'; import { Repository } from 'typeorm'; import { AuthRefreshTokenEntity } from './entities/auth.refresh-token.entity'; import { PinoLogger } from 'nestjs-pino'; @@ -20,7 +20,6 @@ export class AuthRefreshService { private logger: PinoLogger, ) { } - async generate(user: UserEntity, refreshToken?: string) { let expiration: Date | null = null; if (refreshToken) { @@ -35,7 +34,7 @@ export class AuthRefreshService { }); throw new UnauthorizedException('Invalid refresh token.'); } - if (token.exp.getTime() > new Date().getTime()) { + if (token.exp.getTime() < new Date().getTime()) { this.logger.warn({ class: AuthRefreshService.name, method: this.generate.name, @@ -55,13 +54,12 @@ export class AuthRefreshService { // - token has reached expiration threshold; // - token has expired. const now = new Date(); + const expirationTime = parseInt(this.config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_EXPIRATION_MS')); const threshhold = parseInt(this.config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_EXPIRATION_THRESHHOLD_MS')); - if (!refreshToken || now.getTime() - expiration.getTime() > threshhold) { + if (!refreshToken || expirationTime - (expiration.getTime() - now.getTime()) > threshhold) { + let deletionTask = null; if (refreshToken) { - this.authRefreshTokenRepository.delete({ - tokenHash: refreshToken, - userId: user.userId, - }); + deletionTask = this.revoke(user.userId, refreshToken); this.logger.debug({ class: AuthRefreshService.name, @@ -73,15 +71,14 @@ export class AuthRefreshService { }); } - const limit = parseInt(this.config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_EXPIRATION_MS')); - expiration = moment(now).add(limit, 'ms').toDate(); + expiration = moment(now).add(expirationTime, 'ms').toDate(); refreshToken = await this.jwts.signAsync( { username: user.userLogin, sub: user.userId, - iat: now.getTime(), - nbf: now.getTime(), - exp: expiration.getTime(), + iat: Math.floor(now.getTime() / 1000), + nbf: Math.floor(now.getTime() / 1000) - 5 * 60, + exp: Math.floor(expiration.getTime() / 1000), }, { secret: this.config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_SECRET'), @@ -98,7 +95,7 @@ export class AuthRefreshService { }); this.authRefreshTokenRepository.insert({ - tokenHash: refreshToken, + tokenHash: this.hash(refreshToken), userId: user.userId, exp: expiration }); @@ -109,8 +106,10 @@ export class AuthRefreshService { user, refresh_token: refreshToken, exp: expiration, - msg: 'Inserted the new refresh token.', + msg: 'Inserted the new refresh token into the database.', }); + + await deletionTask; } return { @@ -127,15 +126,28 @@ export class AuthRefreshService { return null; } - const buffer = Buffer.from(refreshToken, 'utf8'); - const hash = crypto.createHash('sha256').update(buffer).digest('base64'); - return await this.authRefreshTokenRepository.findOneBy({ - tokenHash: hash, + tokenHash: this.hash(refreshToken), userId: userId, }); } + private hash(refreshToken: string): string { + const buffer = Buffer.from(refreshToken, 'utf8'); + return crypto.createHash('sha256').update(buffer).digest('base64'); + } + + async revoke(userId: UUID, refreshToken: string) { + if (!userId || !refreshToken) { + return null; + } + + return await this.authRefreshTokenRepository.delete({ + userId, + tokenHash: this.hash(refreshToken), + }); + } + async validate( refreshToken: string, userId: UUID, diff --git a/backend/nestjs-seshat-api/src/auth/auth.service.ts b/backend/nestjs-seshat-api/src/auth/auth.service.ts index 2cacfcd..bcc4300 100644 --- a/backend/nestjs-seshat-api/src/auth/auth.service.ts +++ b/backend/nestjs-seshat-api/src/auth/auth.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { UserEntity } from 'src/users/users.entity'; +import { UserEntity } from 'src/users/entities/users.entity'; import { UsersService } from 'src/users/users.service'; import { AuthRefreshService } from './auth.refresh.service'; import { AuthAccessService } from './auth.access.service'; +import { UUID } from 'crypto'; @Injectable() export class AuthService { @@ -26,17 +27,20 @@ export class AuthService { async renew( user: UserEntity, - refresh_token: string - ): Promise { + refresh_token: string | null + ): Promise { const new_refresh_data = await this.refreshTokens.generate(user, refresh_token); - const new_refresh_token = new_refresh_data.refresh_token; - const new_refresh_exp = new_refresh_data.exp; const access_token = await this.accessTokens.generate(user); return { ...access_token, - refresh_token: new_refresh_token, - refresh_exp: new_refresh_exp, + refresh_token: new_refresh_data.refresh_token, + refresh_exp: new_refresh_data.exp, } } + + async revoke(userId: UUID, refreshToken: string): Promise { + const res = await this.refreshTokens.revoke(userId, refreshToken); + return res?.affected === 1 + } } diff --git a/backend/nestjs-seshat-api/src/auth/entities/auth.refresh-token.entity.ts b/backend/nestjs-seshat-api/src/auth/entities/auth.refresh-token.entity.ts index b12231b..34ca1ea 100644 --- a/backend/nestjs-seshat-api/src/auth/entities/auth.refresh-token.entity.ts +++ b/backend/nestjs-seshat-api/src/auth/entities/auth.refresh-token.entity.ts @@ -1,20 +1,16 @@ import * as crypto from 'crypto'; -import { IsNotEmpty } from 'class-validator'; import { UUID } from 'crypto'; import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm'; @Entity("refresh_tokens") export class AuthRefreshTokenEntity { @PrimaryColumn({ name: 'user_id' }) - @IsNotEmpty() readonly userId: UUID; @PrimaryColumn({ name: 'refresh_token_hash' }) - @IsNotEmpty() tokenHash: string; - @Column() - @IsNotEmpty() + @Column({ name: 'exp' }) exp: Date; @BeforeInsert() diff --git a/backend/nestjs-seshat-api/src/auth/guards/jwt-access.admin.guard.ts b/backend/nestjs-seshat-api/src/auth/guards/jwt-access.admin.guard.ts index 29f541c..3378904 100644 --- a/backend/nestjs-seshat-api/src/auth/guards/jwt-access.admin.guard.ts +++ b/backend/nestjs-seshat-api/src/auth/guards/jwt-access.admin.guard.ts @@ -1,17 +1,10 @@ -import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() -export class JwtAccessAdminGuard extends AuthGuard('jwt') { - canActivate(context: ExecutionContext) { - // Add your custom authentication logic here - // for example, call super.logIn(request) to establish a session. - return super.canActivate(context); - } - +export class JwtAccessAdminGuard extends AuthGuard('jwt-access') { handleRequest(err, user, info) { - // You can throw an exception based on either "info" or "err" arguments if (err || !user || !user.isAdmin) { throw err || new UnauthorizedException(); } diff --git a/backend/nestjs-seshat-api/src/auth/guards/jwt-access.guard.ts b/backend/nestjs-seshat-api/src/auth/guards/jwt-access.guard.ts index 36ca511..80bcffe 100644 --- a/backend/nestjs-seshat-api/src/auth/guards/jwt-access.guard.ts +++ b/backend/nestjs-seshat-api/src/auth/guards/jwt-access.guard.ts @@ -1,6 +1,13 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() -export class JwtAccessGuard extends AuthGuard('jwt') { } +export class JwtAccessGuard extends AuthGuard('jwt-access') { + handleRequest(err, user, info) { + if (err || !user || !user.isAdmin) { + throw err || new UnauthorizedException(); + } + return user; + } +} diff --git a/backend/nestjs-seshat-api/src/auth/strategies/jwt-refresh.strategy.ts b/backend/nestjs-seshat-api/src/auth/strategies/jwt-refresh.strategy.ts index 0255bf3..0e3a40c 100644 --- a/backend/nestjs-seshat-api/src/auth/strategies/jwt-refresh.strategy.ts +++ b/backend/nestjs-seshat-api/src/auth/strategies/jwt-refresh.strategy.ts @@ -1,25 +1,54 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; -import { Injectable } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { AuthRefreshService } from '../auth.refresh.service'; import { Request } from 'express'; +import { UsersService } from 'src/users/users.service'; @Injectable() export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { - constructor(private auth: AuthRefreshService, private config: ConfigService) { + constructor(private auth: AuthRefreshService, private users: UsersService, private config: ConfigService) { super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + jwtFromRequest: ExtractJwt.fromExtractors([ + //ExtractJwt.fromAuthHeaderAsBearerToken(), + JwtRefreshStrategy.extract, + ]), ignoreExpiration: false, - secretOrKey: config.get('AUTH_JWT_REFRESH_SECRET'), + secretOrKey: config.getOrThrow('AUTH_JWT_REFRESH_TOKEN_SECRET'), issuer: config.getOrThrow('AUTH_JWT_ISSUER'), audience: config.getOrThrow('AUTH_JWT_AUDIENCE'), passReqToCallback: true, }); } + private static extract(req: any): string | null { + const jwt = req.cookies?.Refresh; + if (!jwt) + return null; + + return jwt; + } + async validate(request: Request, payload: any) { - return this.auth.validate(request.cookies?.Refresh, payload.sub); + const user = await this.users.findById(payload.sub); + if (!user || user.userLogin != payload.username) { + throw new UnauthorizedException(); + } + + if (payload.iss != this.config.getOrThrow('AUTH_JWT_ISSUER')) { + throw new UnauthorizedException(); + } + + if (payload.aud != this.config.getOrThrow('AUTH_JWT_AUDIENCE')) { + throw new UnauthorizedException(); + } + + const refreshToken = request.cookies?.Refresh; + if (!refreshToken || !this.auth.validate(refreshToken, payload.sub)) { + throw new UnauthorizedException(); + } + return user; } } diff --git a/backend/nestjs-seshat-api/src/auth/strategies/jwt.strategy.ts b/backend/nestjs-seshat-api/src/auth/strategies/jwt.strategy.ts index 2105a6a..211adf4 100644 --- a/backend/nestjs-seshat-api/src/auth/strategies/jwt.strategy.ts +++ b/backend/nestjs-seshat-api/src/auth/strategies/jwt.strategy.ts @@ -6,21 +6,43 @@ import { ConfigService } from '@nestjs/config'; import { UsersService } from 'src/users/users.service'; @Injectable() -export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-access') { constructor(private users: UsersService, private config: ConfigService) { super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + jwtFromRequest: ExtractJwt.fromExtractors([ + //ExtractJwt.fromAuthHeaderAsBearerToken(), + JwtStrategy.extract, + ]), ignoreExpiration: false, secretOrKey: config.getOrThrow('AUTH_JWT_ACCESS_TOKEN_SECRET'), issuer: config.getOrThrow('AUTH_JWT_ISSUER'), audience: config.getOrThrow('AUTH_JWT_AUDIENCE'), - passReqToCallback: true, }); } - async validate(req: Request, payload: any) { + private static extract(req: any): string | null { + const jwt = req.cookies?.Authentication; + if (!jwt) + return null; + + return jwt; + } + + async validate(payload: any) { + if (!payload) { + throw new UnauthorizedException(); + } + + if (payload.iss != this.config.getOrThrow('AUTH_JWT_ISSUER')) { + throw new UnauthorizedException(); + } + + if (payload.aud != this.config.getOrThrow('AUTH_JWT_AUDIENCE')) { + throw new UnauthorizedException(); + } + const user = await this.users.findById(payload.sub); - if (!user) { + if (!user || user.userLogin != payload.username) { throw new UnauthorizedException(); } return user; diff --git a/backend/nestjs-seshat-api/src/logging.serializers.ts b/backend/nestjs-seshat-api/src/logging.serializers.ts index b2f1050..965b5c7 100644 --- a/backend/nestjs-seshat-api/src/logging.serializers.ts +++ b/backend/nestjs-seshat-api/src/logging.serializers.ts @@ -1,4 +1,4 @@ -import { UserEntity } from "./users/users.entity"; +import { UserEntity } from "./users/entities/users.entity"; export function serialize_user_short(value: UserEntity) { if (!value) { @@ -29,13 +29,29 @@ export function serialize_req(value) { return value; } + value = { ...value }; + delete value['remoteAddress'] delete value['remotePort'] if (value.headers) { - const headers = value.headers; - if (headers['Authorization']) { - headers['Authorization'] = headers['Authorization'].substring(Math.max(0, headers['Authorization'].length - 12)) + const headers = value.headers = { ...value.headers }; + if (headers['authorization']) { + headers['authorization'] = '...' + headers['authorization'].substring(Math.max(0, headers['authorization'].length - 16)) + } + + if (headers['cookie']) { + const cookies = headers['cookie'].split(';') + .map((c: string) => { + c = c.trim(); + const index = c.indexOf('='); + if (index < 0) + return c; + + const cookieValue = c.substring(index + 1); + return c.substring(0, index) + '=...' + cookieValue.substring(Math.max(cookieValue.length - 16, cookieValue.length / 2)); + }); + headers['cookie'] = cookies.join('; '); } } return value; @@ -46,7 +62,8 @@ export function serialize_res(value) { return value; } - const headers = value.headers; + value = { ...value }; + const headers = value.headers = { ...value.headers }; delete headers['x-powered-by']; if (headers['set-cookie']) { @@ -61,4 +78,4 @@ export function serialize_res(value) { } } return value; -} \ No newline at end of file +}