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:
Key Components
- JwtService: Handles JWT token creation, signing, and verification
- AuthService: Provides high-level authentication operations
- AuthGuard: Protects routes and validates tokens
- @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:
- Method Interception: Wraps your method with authentication logic
- Token Extraction: Extracts JWT token from
Authorizationheader - Token Validation: Verifies token using
AuthServiceandJwtService - User Injection: Attaches user object to request context
- Role Checking: Validates user roles if specified
- 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
Authorizationheader - 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:
- Always validate roles server-side: Don't rely on client-side role checks
- Use specific roles: Be explicit about required permissions
- Handle errors gracefully: Use exception filters for custom error responses
- Token expiration: Ensure tokens have appropriate expiration times
- 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
-
Secure token storage: Never store tokens in localStorage for web apps. Use httpOnly cookies instead.
-
Token expiration: Always set appropriate expiration times for tokens.
-
Refresh tokens: Implement refresh token mechanism for long-lived sessions.
-
Role-based access: Use the
rolesoption in@Auth()decorator for fine-grained access control. -
HTTPS only: Always use HTTPS in production to protect tokens in transit.