Files
goodgo-platform/apps/api/src/modules/shared/shared.module.ts
Ho Ngoc Hai 6389dcf78e fix(auth): migrate tokens from localStorage to httpOnly cookies + CSRF hardening
Backend:
- Auth controller sets httpOnly secure cookies (access_token, refresh_token, goodgo_authenticated) on login/register/refresh
- JWT strategy reads token from cookie first, falls back to Authorization header
- Added POST /auth/logout to clear auth cookies
- Added POST /auth/exchange-token for OAuth callback token-to-cookie exchange
- Refresh endpoint reads refresh_token from cookie (body fallback for backwards compat)
- CSRF middleware excludes auth endpoints (login, register, refresh, exchange-token, logout)

Frontend:
- Removed all localStorage token storage (goodgo_tokens key)
- Removed authGet/authPost/authPatch helpers from api-client (tokens sent via cookies)
- All API calls use credentials:'include' for cookie-based auth
- Updated auth-store: no more token state, uses isAuthenticated flag from cookie
- Updated admin-api, listings-api to remove explicit token parameters
- Updated all pages (admin dashboard, users, KYC, moderation, listings) to remove token passing
- OAuth callbacks use exchange-token endpoint to convert URL tokens to cookies
- Auth provider simplified (no client-side cookie management needed)

Security improvements:
- JWT no longer accessible via JavaScript (XSS-safe)
- Refresh token scoped to /auth path only
- Server-side goodgo_authenticated cookie with SameSite=Lax
- Access token cookie with SameSite=Strict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:25:11 +07:00

63 lines
2.4 KiB
TypeScript

import { Global, type MiddlewareConsumer, Module, type NestModule, RequestMethod } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { makeCounterProvider } from '@willsoto/nestjs-prometheus';
import { EventBusService } from './infrastructure/event-bus.service';
import { GlobalExceptionFilter } from './infrastructure/filters/global-exception.filter';
import { LoggerService } from './infrastructure/logger.service';
import { CorrelationIdMiddleware } from './infrastructure/middleware/correlation-id.middleware';
import { CsrfMiddleware } from './infrastructure/middleware/csrf.middleware';
import { RequestLoggingMiddleware } from './infrastructure/middleware/request-logging.middleware';
import { SanitizeInputMiddleware } from './infrastructure/middleware/sanitize-input.middleware';
import { PrismaService } from './infrastructure/prisma.service';
import { RedisService } from './infrastructure/redis.service';
import { CacheService, CACHE_HIT_TOTAL, CACHE_MISS_TOTAL } from './infrastructure/cache.service';
@Global()
@Module({
imports: [EventEmitterModule.forRoot()],
providers: [
PrismaService,
RedisService,
CacheService,
LoggerService,
EventBusService,
makeCounterProvider({
name: CACHE_HIT_TOTAL,
help: 'Total number of cache hits',
labelNames: ['resource'],
}),
makeCounterProvider({
name: CACHE_MISS_TOTAL,
help: 'Total number of cache misses',
labelNames: ['resource'],
}),
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter,
},
],
exports: [PrismaService, RedisService, CacheService, LoggerService, EventBusService],
})
export class SharedModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(CorrelationIdMiddleware, SanitizeInputMiddleware, RequestLoggingMiddleware)
.forRoutes('*');
if (process.env['NODE_ENV'] !== 'test') {
consumer
.apply(CsrfMiddleware)
.exclude(
{ path: 'payments/callback/(.*)', method: RequestMethod.POST },
{ path: 'auth/login', method: RequestMethod.POST },
{ path: 'auth/register', method: RequestMethod.POST },
{ path: 'auth/refresh', method: RequestMethod.POST },
{ path: 'auth/exchange-token', method: RequestMethod.POST },
{ path: 'auth/logout', method: RequestMethod.POST },
)
.forRoutes('*');
}
}
}