All files / src/common/guards jwt-auth.guard.ts

45% Statements 18/40
36% Branches 9/25
100% Functions 4/4
42.1% Lines 16/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 7627x 27x 27x     27x 145x 145x       151x 151x     151x 105x     151x         105x 105x                                                                                       152x   18x   134x    
import { ExecutionContext, Injectable, UnauthorizedException, ForbiddenException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ConfigService } from '@nestjs/config';
 
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private readonly config?: ConfigService) {
    super();
  }
 
  canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    const method = request.method;
    
    // CSRF protection for state-changing methods (POST, PUT, DELETE, PATCH)
    if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
      this.validateCsrf(request);
    }
    
    return super.canActivate(context) as any;
  }
 
  private validateCsrf(request: any): void {
    // Skip CSRF validation in test mode
    if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE === '1') {
      return;
    }
 
    const origin = request.headers.origin;
    const referer = request.headers.referer;
    const frontendUrl = this.config?.get<string>('FRONTEND_URL');
 
    // If FRONTEND_URL is configured, validate Origin header matches
    Iif (frontendUrl) {
      try {
        const expectedOrigin = new URL(frontendUrl).origin.toLowerCase();
        if (origin) {
          const requestOrigin = new URL(origin).origin.toLowerCase();
          Iif (requestOrigin !== expectedOrigin) {
            throw new ForbiddenException('CSRF validation failed: Invalid origin');
          }
        } else if (referer) {
          // Fallback to Referer header if Origin is missing
          const refererOrigin = new URL(referer).origin.toLowerCase();
          Iif (refererOrigin !== expectedOrigin) {
            throw new ForbiddenException('CSRF validation failed: Invalid referer');
          }
        } else {
          // No Origin or Referer header - require X-Requested-With header as additional protection
          // This header cannot be set by simple HTML forms (CSRF protection)
          const requestedWith = request.headers['x-requested-with'];
          Iif (!requestedWith || requestedWith.toLowerCase() !== 'xmlhttprequest') {
            throw new ForbiddenException('CSRF validation failed: Missing required headers');
          }
        }
      } catch (error) {
        Iif (error instanceof ForbiddenException) {
          throw error;
        }
        // If URL parsing fails, fall through to X-Requested-With check
        const requestedWith = request.headers['x-requested-with'];
        Iif (!requestedWith || requestedWith.toLowerCase() !== 'xmlhttprequest') {
          throw new ForbiddenException('CSRF validation failed: Invalid request format');
        }
      }
    }
  }
 
  handleRequest(err: any, user: any, _info: any) {
    if (err || !user) {
      // Hide token parsing specifics; standardize error
      throw new UnauthorizedException('Authentication required');
    }
    return user;
  }
}