Auth Package

The @hazeljs/auth package provides authentication and authorization capabilities for your HazelJS applications. It includes JWT token management, authentication guards, and role-based access control.

Purpose

Securing your application requires implementing authentication (verifying who users are) and authorization (determining what they can do). Building this from scratch involves handling JWT tokens, validating credentials, protecting routes, and managing user sessions. The @hazeljs/auth package simplifies this by providing:

  • JWT Management: Complete JWT token creation, signing, and verification
  • Route Protection: Decorator-based guards to protect endpoints
  • Role-Based Access Control: Built-in support for role-based permissions
  • Token Validation: Automatic token verification and user extraction
  • Flexible Architecture: Easy to extend with custom authentication logic

Architecture

The package follows a guard-based architecture that integrates seamlessly with HazelJS's request lifecycle:

Loading diagram...

Key Components

  1. JwtService: Handles JWT token creation, signing, and verification
  2. AuthService: Provides high-level authentication operations
  3. AuthGuard: Protects routes and validates tokens
  4. @Auth Decorator: Simple decorator for route protection

Advantages

1. Security First

Built with security best practices, including secure token handling, proper error messages, and protection against common vulnerabilities.

2. Simple API

Protect routes with a single @Auth() decorator. No need to manually check tokens or extract user information.

3. Flexible Permissions

Support for role-based access control (RBAC) allows fine-grained permission management. Protect admin routes, user-specific data, and more.

4. Type Safety

Full TypeScript support ensures type-safe user objects and request contexts.

5. Extensible

Easy to create custom guards for API keys, OAuth, or other authentication methods.

6. Production Ready

Includes proper error handling, token expiration management, and integration with HazelJS's dependency injection.

Installation

npm install @hazeljs/auth

Quick Start

Basic Setup

Import and register the auth module:

import { HazelModule } from '@hazeljs/core';
import { JwtModule } from '@hazeljs/auth';

@HazelModule({
  imports: [
    JwtModule,
  ],
  providers: [AuthService],
})
export class AppModule {}

Authentication Service

The AuthService provides token verification functionality:

import { Injectable } from '@hazeljs/core';
import { AuthService, JwtService } from '@hazeljs/auth';

@Injectable()
export class UserAuthService {
  constructor(
    private readonly authService: AuthService,
    private readonly jwtService: JwtService,
  ) {}

  async login(email: string, password: string) {
    // Validate credentials
    const user = await this.validateUser(email, password);
    
    if (!user) {
      throw new Error('Invalid credentials');
    }

    // Generate JWT token
    const token = await this.jwtService.signAsync({
      sub: user.id,
      email: user.email,
    });

    return { token, user };
  }

  async verifyToken(token: string) {
    return await this.authService.verifyToken(token);
  }
}

JWT Service

The JwtService handles JWT token creation and verification:

import { Injectable } from '@hazeljs/core';
import { JwtService } from '@hazeljs/auth';

@Injectable()
export class TokenService {
  constructor(private readonly jwtService: JwtService) {}

  async createToken(payload: { userId: number; email: string }) {
    return await this.jwtService.signAsync({
      sub: payload.userId,
      email: payload.email,
    });
  }

  async validateToken(token: string) {
    try {
      return await this.jwtService.verifyAsync(token);
    } catch {
      return null;
    }
  }
}

Authentication Guard

Use the AuthGuard to protect routes:

import { Controller, Get, Post, Body } from '@hazeljs/core';
import { AuthGuard, Auth } from '@hazeljs/auth';

@Controller('users')
export class UsersController {
  @Get('profile')
  @Auth() // Protect this route
  async getProfile(@Req() req: Request) {
    // req.user is automatically populated by the guard
    return req.user;
  }

  @Get('admin')
  @Auth({ roles: ['admin'] }) // Require admin role
  async getAdminData() {
    return { message: 'Admin only data' };
  }
}

@Auth Decorator

The @Auth decorator is a method decorator that protects routes by requiring authentication. It integrates with HazelJS's guard system to validate requests before they reach your handler.

Understanding @Auth Decorator

Purpose: Marks a route handler as requiring authentication. Automatically validates JWT tokens and extracts user information.

How it works:

  1. Method Interception: Wraps your method with authentication logic
  2. Token Extraction: Extracts JWT token from Authorization header
  3. Token Validation: Verifies token using AuthService and JwtService
  4. User Injection: Attaches user object to request context
  5. Role Checking: Validates user roles if specified
  6. Error Handling: Throws appropriate HTTP errors for failures

Configuration Options:

interface AuthGuardOptions {
  roles?: string[];  // Required roles for access (e.g., ['admin', 'moderator'])
}

Example with Detailed Explanation:

import { Controller, Get, Req } from '@hazeljs/core';
import { Auth } from '@hazeljs/auth';
import { Request } from 'express';

@Controller('users')
export class UsersController {
  @Get('profile')
  // @Auth() is a method decorator that requires authentication
  @Auth() // No options = any authenticated user can access
  async getProfile(@Req() req: Request) {
    // When this route is called:
    // 1. Framework extracts token from Authorization header
    // 2. Validates token using JwtService
    // 3. Extracts user information from token payload
    // 4. Attaches user to req.user
    // 5. If validation fails, throws 401 Unauthorized
    // 6. If successful, executes your method
    
    // req.user is automatically populated by the guard
    // Contains: { id, email, role, ... } from token payload
    return req.user;
  }

  @Get('admin')
  // @Auth with roles option requires specific permissions
  @Auth({ roles: ['admin'] })
  // Only users with 'admin' role can access
  async getAdminData() {
    // Authentication flow:
    // 1. Validates token (same as above)
    // 2. Checks if user.role includes 'admin'
    // 3. If not admin, throws 403 Forbidden
    // 4. If admin, executes method
    return { message: 'Admin only data' };
  }

  @Get('moderator')
  // Multiple roles - user needs at least one
  @Auth({ roles: ['admin', 'moderator'] })
  async getModeratorData() {
    // User must have 'admin' OR 'moderator' role
    return { message: 'Moderator or admin data' };
  }
}

Error Responses:

The @Auth decorator throws specific HTTP errors:

  • 400 Bad Request: Missing or malformed Authorization header
  • 401 Unauthorized: Invalid or expired token
  • 403 Forbidden: Valid token but insufficient permissions (wrong role)

Token Format:

The decorator expects tokens in the standard format:

Authorization: Bearer <token>

Request Context:

After successful authentication, the user object is available in the request:

@Get('me')
@Auth()
async getCurrentUser(@Req() req: Request) {
  // req.user contains:
  // {
  //   id: number,        // From token.sub
  //   email: string,     // From token.email
  //   role: string,      // From token or default
  //   ...other token claims
  // }
  return req.user;
}

Best Practices:

  1. Always validate roles server-side: Don't rely on client-side role checks
  2. Use specific roles: Be explicit about required permissions
  3. Handle errors gracefully: Use exception filters for custom error responses
  4. Token expiration: Ensure tokens have appropriate expiration times
  5. Refresh tokens: Implement refresh token mechanism for long sessions

Custom Authentication Guard

Create a custom guard for specific authentication logic:

import { Injectable } from '@hazeljs/core';
import { AuthGuard, IAuthGuard, RequestContext } from '@hazeljs/auth';

@Injectable()
export class CustomAuthGuard implements IAuthGuard {
  async canActivate(context: RequestContext, options?: AuthGuardOptions): Promise<boolean> {
    // Custom authentication logic
    const apiKey = context.headers['x-api-key'];
    
    if (!apiKey) {
      throw new Error('API key required');
    }

    // Validate API key
    const isValid = await this.validateApiKey(apiKey);
    
    if (!isValid) {
      throw new Error('Invalid API key');
    }

    return true;
  }

  private async validateApiKey(key: string): Promise<boolean> {
    // Your validation logic
    return key === process.env.API_KEY;
  }
}

Complete Example

Here's a complete authentication flow:

import { Injectable } from '@hazeljs/core';
import { Controller, Get, Post, Body, Req } from '@hazeljs/core';
import { AuthService, JwtService, Auth } from '@hazeljs/auth';

// Service
@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}

  async login(email: string, password: string) {
    // Validate user credentials
    const user = await this.validateCredentials(email, password);
    
    if (!user) {
      throw new Error('Invalid credentials');
    }

    // Generate token
    const token = await this.jwtService.signAsync({
      sub: user.id,
      email: user.email,
    });

    return { token, user };
  }

  async verifyToken(token: string) {
    try {
      const payload = await this.jwtService.verifyAsync(token);
      return {
        id: payload.sub,
        email: payload.email,
        role: 'user',
      };
    } catch {
      return null;
    }
  }

  private async validateCredentials(email: string, password: string) {
    // Your credential validation logic
    return { id: 1, email };
  }
}

// Controller
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  async login(@Body() body: { email: string; password: string }) {
    return await this.authService.login(body.email, body.password);
  }

  @Get('me')
  @Auth()
  async getCurrentUser(@Req() req: Request) {
    return req.user;
  }
}

@Controller('users')
export class UsersController {
  @Get('profile')
  @Auth()
  async getProfile(@Req() req: Request) {
    return { user: req.user };
  }

  @Get('admin')
  @Auth({ roles: ['admin'] })
  async getAdminData() {
    return { message: 'Admin data' };
  }
}

Environment Variables

Configure JWT secret:

JWT_SECRET=your-secret-key-here

Best Practices

  1. Secure token storage: Never store tokens in localStorage for web apps. Use httpOnly cookies instead.

  2. Token expiration: Always set appropriate expiration times for tokens.

  3. Refresh tokens: Implement refresh token mechanism for long-lived sessions.

  4. Role-based access: Use the roles option in @Auth() decorator for fine-grained access control.

  5. HTTPS only: Always use HTTPS in production to protect tokens in transit.

What's Next?

  • Learn about Config for managing secrets
  • Explore Cache for token caching
  • Check out WebSocket for real-time authentication