Skip to content

Guards & Strategies

Complete guide to authentication and authorization guards in CodeNotify.

Overview

Guards control access to routes and endpoints. CodeNotify uses two main guards:

  1. JwtAuthGuard - Validates JWT tokens (authentication)
  2. RolesGuard - Checks user roles (authorization)

JWT Auth Guard

Purpose

Protects routes by validating JWT access tokens. Applied globally to all routes except those marked with @Public().

Implementation

File: server/src/auth/guards/jwt-auth.guard.ts

typescript
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext) {
    // Check if route is marked as @Public()
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) {
      return true;  // Skip authentication
    }

    return super.canActivate(context);  // Validate JWT
  }
}

How It Works

Request → JwtAuthGuard → Check @Public() → Yes → Allow

                             No

                         Validate JWT → Valid → Allow

                           Invalid

                          401 Unauthorized

Global Application

File: server/src/main.ts

typescript
const app = await NestFactory.create(AppModule);

// Apply JWT guard globally
app.useGlobalGuards(new JwtAuthGuard(app.get(Reflector)));

All routes are protected by default unless marked @Public().

Usage Examples

Protected Route (Default)

typescript
@Controller('users')
export class UsersController {
  @Get('profile')
  getProfile(@CurrentUser() user: UserDocument) {
    // JWT required - automatically enforced
    return user;
  }
}

Public Route

typescript
@Controller('auth')
export class AuthController {
  @Public()  // Skip JWT validation
  @Post('signin')
  signin(@Body() dto: SigninDto) {
    return this.authService.signin(dto);
  }
}

Class-Level Protection

typescript
@Controller('admin')
@UseGuards(JwtAuthGuard)  // Protect all routes
export class AdminController {
  @Get('users')
  getAllUsers() {
    // JWT required
  }

  @Get('stats')
  getStats() {
    // JWT required
  }
}

Roles Guard

Purpose

Enforces role-based access control (RBAC). Checks if authenticated user has required role(s).

Implementation

File: server/src/auth/guards/roles.guard.ts

typescript
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // Get required roles from @Roles() decorator
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    // No roles required - allow access
    if (!requiredRoles) {
      return true;
    }

    // Get user from request (populated by JwtStrategy)
    const request = context.switchToHttp().getRequest<RequestWithUser>();
    const user = request.user;

    // No user - deny access
    if (!user) {
      return false;
    }

    // Check if user has any of the required roles
    return requiredRoles.some((role) => user.role === role);
  }
}

How It Works

Request → RolesGuard → Check @Roles() → None → Allow

                       Has @Roles()

                       Get user.role

                       Match required? → Yes → Allow

                           No

                       403 Forbidden

Role Types

File: server/src/auth/decorators/roles.decorator.ts

typescript
export type Role = 'user' | 'admin';
  • user: Regular user (default)
  • admin: Administrator with elevated privileges

Usage Examples

Admin-Only Route

typescript
@Controller('admin')
export class AdminController {
  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @Delete('users/:id')
  deleteUser(@Param('id') id: string) {
    // Only admin can access
    return this.usersService.deleteUser(id);
  }
}

Multiple Roles

typescript
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin', 'moderator')
@Post('contests/sync')
syncContests() {
  // Admin OR moderator can access
  return this.contestsService.syncAllPlatforms();
}

Class-Level Roles

typescript
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')  // All routes require admin
export class AdminController {
  @Get('users')
  getAllUsers() {
    // Admin only
  }

  @Get('stats')
  getStats() {
    // Admin only
  }
}

Decorators

@Public()

Marks routes as public (skip JWT authentication).

File: server/src/common/decorators/public.decorator.ts

typescript
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

Usage:

typescript
@Public()
@Post('signup')
signup(@Body() dto: CreateUserDto) {
  // No authentication required
}

@Roles()

Specifies required roles for a route.

File: server/src/auth/decorators/roles.decorator.ts

typescript
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

Usage:

typescript
@Roles('admin')
@Delete('users/:id')
deleteUser(@Param('id') id: string) {
  // Only admin can access
}

@CurrentUser()

Extracts authenticated user from request.

File: server/src/common/decorators/current-user.decorator.ts

typescript
export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext): User => {
    return ctx.switchToHttp().getRequest<{ user: User }>().user;
  },
);

Usage:

typescript
@Get('profile')
getProfile(@CurrentUser() user: UserDocument) {
  // user is populated by JwtStrategy
  return user;
}

Guard Execution Order

Multiple Guards

When using multiple guards, they execute in order:

typescript
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get('admin/users')
getUsers() {
  // 1. JwtAuthGuard validates token
  // 2. RolesGuard checks role
}

Execution Flow

Request

JwtAuthGuard
  ├─ Check @Public() → Yes → Skip to RolesGuard
  └─ No → Validate JWT
      ├─ Valid → Continue to RolesGuard
      └─ Invalid → 401 Unauthorized

RolesGuard
  ├─ Check @Roles() → None → Allow
  └─ Has @Roles()
      ├─ Check user.role → Match → Allow
      └─ No match → 403 Forbidden

Controller Method

Error Responses

401 Unauthorized (JwtAuthGuard)

Causes:

  • Missing Authorization header
  • Invalid token format
  • Expired token
  • Invalid signature
  • User not found
  • User deactivated

Response:

json
{
  "statusCode": 401,
  "message": "Unauthorized",
  "error": "Unauthorized"
}

403 Forbidden (RolesGuard)

Causes:

  • User authenticated but lacks required role
  • User is 'user' but route requires 'admin'

Response:

json
{
  "statusCode": 403,
  "message": "Forbidden resource",
  "error": "Forbidden"
}

Common Patterns

Pattern 1: Public Endpoint

typescript
@Public()
@Post('auth/signin')
signin(@Body() dto: SigninDto) {
  // Anyone can access
}

Pattern 2: Authenticated Endpoint

typescript
@Get('users/profile')
getProfile(@CurrentUser() user: UserDocument) {
  // JWT required (global guard)
}

Pattern 3: Admin-Only Endpoint

typescript
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Delete('users/:id')
deleteUser(@Param('id') id: string) {
  // JWT + admin role required
}

Pattern 4: Multiple Roles

typescript
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin', 'moderator')
@Post('contests/sync')
syncContests() {
  // JWT + (admin OR moderator) required
}

Pattern 5: Class-Level Protection

typescript
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
export class AdminController {
  // All routes require JWT + admin role
  
  @Get('users')
  getUsers() {}
  
  @Get('stats')
  getStats() {}
}

Testing

Unit Tests - JwtAuthGuard

typescript
describe('JwtAuthGuard', () => {
  let guard: JwtAuthGuard;
  let reflector: Reflector;

  beforeEach(() => {
    reflector = new Reflector();
    guard = new JwtAuthGuard(reflector);
  });

  it('should allow access to public routes', () => {
    const context = createMockContext();
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(true);

    expect(guard.canActivate(context)).toBe(true);
  });

  it('should validate JWT for protected routes', () => {
    const context = createMockContext();
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(false);

    // Should call super.canActivate() which validates JWT
    expect(guard.canActivate(context)).toBeDefined();
  });
});

Unit Tests - RolesGuard

typescript
describe('RolesGuard', () => {
  let guard: RolesGuard;
  let reflector: Reflector;

  beforeEach(() => {
    reflector = new Reflector();
    guard = new RolesGuard(reflector);
  });

  it('should allow access when no roles required', () => {
    const context = createMockContext();
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);

    expect(guard.canActivate(context)).toBe(true);
  });

  it('should allow access when user has required role', () => {
    const context = createMockContext({ user: { role: 'admin' } });
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);

    expect(guard.canActivate(context)).toBe(true);
  });

  it('should deny access when user lacks required role', () => {
    const context = createMockContext({ user: { role: 'user' } });
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);

    expect(guard.canActivate(context)).toBe(false);
  });
});

E2E Tests

typescript
describe('Guards E2E', () => {
  it('should allow access to public routes without token', () => {
    return request(app.getHttpServer())
      .post('/auth/signin')
      .send(credentials)
      .expect(200);
  });

  it('should deny access to protected routes without token', () => {
    return request(app.getHttpServer())
      .get('/users/profile')
      .expect(401);
  });

  it('should allow access to protected routes with valid token', () => {
    return request(app.getHttpServer())
      .get('/users/profile')
      .set('Authorization', `Bearer ${validToken}`)
      .expect(200);
  });

  it('should deny access to admin routes for regular users', () => {
    return request(app.getHttpServer())
      .delete('/users/123')
      .set('Authorization', `Bearer ${userToken}`)
      .expect(403);
  });

  it('should allow access to admin routes for admin users', () => {
    return request(app.getHttpServer())
      .delete('/users/123')
      .set('Authorization', `Bearer ${adminToken}`)
      .expect(200);
  });
});

Best Practices

✅ Do

  1. Apply guards in correct order (JwtAuthGuard before RolesGuard)
  2. Use @Public() sparingly (only for truly public endpoints)
  3. Use class-level guards for consistent protection
  4. Test guard behavior thoroughly
  5. Log authorization failures for security monitoring
  6. Use specific roles (avoid overly permissive access)

❌ Don't

  1. Don't skip JwtAuthGuard when using RolesGuard
  2. Don't make sensitive routes public
  3. Don't check roles manually in controllers (use RolesGuard)
  4. Don't ignore 403 errors (indicates authorization issue)
  5. Don't grant admin role without proper validation

Troubleshooting

Issue: 401 on Protected Route

Symptoms: Getting 401 even with valid token

Checks:

  1. Token in Authorization header: Bearer <token>
  2. Token not expired (15 minutes)
  3. User exists in database
  4. User is active (isActive: true)

Issue: 403 on Admin Route

Symptoms: Authenticated but getting 403

Checks:

  1. User role is 'admin' (check database)
  2. @Roles('admin') decorator present
  3. RolesGuard applied
  4. Token contains correct role

Issue: Public Route Requiring Auth

Symptoms: Public route returning 401

Checks:

  1. @Public() decorator present
  2. Decorator on correct method/class
  3. Global guard configured correctly

Built with ❤️ for competitive programmers worldwide.