npm package: @science-and-humans/science-and-humans-auth-sdk
Description: Authentication sdk for Science and Humans microservices with hybrid role system support
Author: Science and Humans
Latest version: 2.0.1

# Auth Middleware SDK

A lightweight, production-ready authentication middleware for NestJS applications with Clerk integration and Redis fallback support.


### System Design

## Auth Design
![Auth Microservice Architecture](docs/architecture/auth-arc.png)

## SDK Design
![SDK Design](docs/architecture/sdk-arc.png)


## Features

- 🔐 **Clerk Authentication**: Full integration with Clerk authentication service
- 🚀 **NestJS Native**: Built specifically for NestJS with guards, decorators, and modules
- ⚡ **Express Support**: Drop-in middleware and helpers for Express apps
- 🔄 **Redis Fallback**: Automatic fallback from Redis to HTTP when microservice is unavailable
- 🛡️ **Smart Error Handling**: Distinguishes between authentication and infrastructure errors
- 📦 **Type Safety**: Full TypeScript support with comprehensive type definitions
- 🎯 **Role-Based Access**: Support for roles and permissions with decorators
- 🏢 **Organization Authorization**: Advanced organization-level role and permission management
- 📊 **Health Monitoring**: Built-in health checks and service monitoring

## Installation

```bash
npm install @science-and-humans/science-and-humans-auth-sdk
```

## Quick Start

### 1. Module Configuration

```typescript
import { Module } from '@nestjs/common';
import { AuthMiddlewareModule } from '@science-and-humans/science-and-humans-auth-sdk';

@Module({
  imports: [
    AuthMiddlewareModule.forRoot({
      authServiceUrl: 'http://localhost:3000',
      serviceKey: 'your-service-key',
      serviceName: 'your-service-name',
      redis: {
        host: 'localhost',
        port: 6379,
        password: 'your-redis-password', // optional
        db: 0,
      },
      requestTimeout: 5000, // optional, default: 8000ms
    }),
  ],
})
export class AppModule {}
```

### 2. Using Guards and Decorators

```typescript
import { Controller, Get, UseGuards } from '@nestjs/common';
import {
  AuthGuard,
  RolesGuard,
  RequireRoles,
  RequireOrgRole,
  CurrentUser,
  UserRoles,
  GetUserRoleInfo,
  Public,
  ClerkUser,
  UserRoleInfo,
} from '@science-and-humans/science-and-humans-auth-sdk';

@Controller('api')
@UseGuards(AuthGuard, RolesGuard)
export class ApiController {
  @Get('public')
  @Public() // Skip authentication
  getPublicData() {
    return { message: 'Public endpoint' };
  }

  @Get('protected')
  getProtectedData(@CurrentUser() user: ClerkUser) {
    return { message: 'Protected endpoint', user };
  }

  @Get('admin')
  @RequireRoles('admin', 'moderator')
  getAdminData(@CurrentUser() user: ClerkUser, @UserRoles() roles: string[]) {
    return { message: 'Admin only', user, roles };
  }

  @Get('organization/dashboard')
  @RequireOrgRole('org:admin', 'org:moderator')
  getOrgDashboard(
    @CurrentUser() user: ClerkUser,
    @GetUserRoleInfo() roleInfo: UserRoleInfo
  ) {
    return {
      message: 'Organization dashboard',
      user: user.id,
      organizationId: roleInfo.organizationId,
      orgRole: roleInfo.orgRole,
    };
  }
}
```

### AuthGuard Behavior

The `AuthGuard` now works in an **opt-in** manner for better scalability:

- **By default**: Routes are accessible without authentication
- **With `@RequireAuth()`**: Routes require authentication
- **With `@Public()`**: Routes are explicitly marked as public (overrides `@RequireAuth()`)

#### Usage Examples

```typescript
import { 
  AuthGuard, 
  RequireAuth, 
  Public, 
  RequireRoles,
  RequirePermissions 
} from '@science-and-humans/science-and-humans-auth-sdk';

@Controller('api')
@UseGuards(AuthGuard) // Apply guard to all routes in controller
export class ApiController {

  @Get('public')
  @Public() // Explicitly mark as public (overrides guard)
  getPublicData() {
    return { message: 'This is public data' };
  }

  @Get('protected')
  @RequireAuth() // Require authentication
  getProtectedData() {
    return { message: 'This is protected data' };
  }

  @Get('admin-only')
  @RequireAuth()
  @RequireRoles('admin') // Require specific role
  getAdminData() {
    return { message: 'Admin only data' };
  }

  @Get('user-data')
  @RequireAuth()
  @RequirePermissions('read:user') // Require specific permission
  getUserData() {
    return { message: 'User data' };
  }

  @Get('default')
  // No decorator - accessible without authentication by default
  getDefaultData() {
    return { message: 'Default accessible data' };
  }
}
```

#### Guard Priority

1. `@Public()` - Always allows access (highest priority)
2. `@RequireAuth()` - Requires authentication
3. No decorator - Allows access by default (lowest priority)

This approach provides better scalability by allowing you to:
- Start with open access and gradually secure routes as needed
- Avoid accidentally exposing routes due to missing decorators
- Have fine-grained control over authentication requirements
```

## API Reference

### Core Exports

#### AuthClient

The core authentication client that handles all communication with the auth service. Provides automatic fallback from Redis to HTTP and intelligent error handling.

```typescript
import { AuthClient } from '@science-and-humans/science-and-humans-auth-sdk';

const authClient = new AuthClient({
  authServiceUrl: 'http://localhost:3000',
  redis: { host: 'localhost', port: 6379 },
});

// Validate JWT token - Primary authentication method
// Verifies token signature, expiration, and retrieves user data
const validation = await authClient.validateToken({ token: 'jwt-token' });
// Returns: { isValid: boolean, user?: ClerkUser, roles?: string[], permissions?: string[] }

// Validate session - Alternative authentication using session ID
// Useful for session-based authentication flows
const session = await authClient.validateSession({ sessionId: 'session-id' });
// Returns: { isValid: boolean, user?: ClerkUser, roles?: string[], permissions?: string[] }

// Check user authorization - Fine-grained permission checking
// Verifies if user has permission to perform specific actions on resources
const auth = await authClient.authorizeUser({
  userId: 'user-123',
  resource: 'posts',
  action: 'create',
});
// Returns: { isAuthorized: boolean, error?: string }

// Health check - Monitor service connectivity and performance
// Checks Redis connection, HTTP fallback, and overall system health
const health = await authClient.healthCheck();
// Returns: { status: string, services: { redis: string, http: string } }

// Cleanup - Properly close connections and cleanup resources
// Important for graceful application shutdown
await authClient.disconnect();
```

#### AuthMiddlewareModule

NestJS module for authentication setup.

```typescript
import { AuthMiddlewareModule } from '@science-and-humans/science-and-humans-auth-sdk';

// Static configuration
AuthMiddlewareModule.forRoot(config);

// Async configuration
AuthMiddlewareModule.forRootAsync({
  inject: [ConfigService],
  useFactory: async (configService: ConfigService) => ({
    authServiceUrl: configService.get('AUTH_SERVICE_URL'),
    redis: {
      host: configService.get('REDIS_HOST'),
      port: configService.get('REDIS_PORT'),
    },
  }),
});
```

### Guards

#### AuthGuard

Core authentication guard that validates tokens and establishes user context. Automatically extracts tokens from Authorization header, validates them, and populates request.auth with user data.

```typescript
import { AuthGuard } from '@science-and-humans/science-and-humans-auth-sdk';

@UseGuards(AuthGuard)
@Controller('protected')
export class ProtectedController {}
// All routes in this controller require valid authentication
// Populates @CurrentUser() with authenticated user data
```

#### RolesGuard

Role-based access control guard that works in conjunction with AuthGuard. Checks user roles and permissions against route requirements defined by decorators.

```typescript
import { RolesGuard } from '@science-and-humans/science-and-humans-auth-sdk';

@UseGuards(AuthGuard, RolesGuard) // Must be used together
@Controller('admin')
export class AdminController {}
// Enforces role requirements set by @RequireRoles() decorator
// Automatically denies access if user lacks required roles
```

### Decorators

#### Authentication Decorators

Route-level decorators that control authentication and authorization behavior. These work with guards to enforce security policies.

```typescript
import {
  RequireAuth,
  RequireRoles,
  RequirePermissions,
  Public
} from '@science-and-humans/science-and-humans-auth-sdk';

@Public()
// Skip authentication entirely - accessible to anonymous users
// Overrides any guards applied at controller level

@RequireAuth()
// Explicitly require authentication (default behavior when AuthGuard is used)
// Redundant but useful for documentation purposes

@RequireRoles('admin', 'moderator')
// Require user to have one of the specified roles
// User must have 'admin' OR 'moderator' role to access

@RequirePermissions('read:posts', 'write:posts')
// Require specific permissions for fine-grained access control
// User must have 'read:posts' AND 'write:posts' permissions

#### Organization Authorization Decorators

Advanced decorators for organization-level access control. These decorators require users to be in an organization context and have specific organization roles or permissions.

```typescript
import {
  RequireOrgRole,
  RequireOrgPermission,
} from '@science-and-humans/science-and-humans-auth-sdk';

@RequireOrgRole('org:admin', 'org:moderator')
// Require user to have one of the specified organization roles
// User must be in organization context AND have 'org:admin' OR 'org:moderator' role

@RequireOrgPermission('org:manage_members', 'org:view_analytics')
// Require specific organization permissions for fine-grained access control
// User must be in organization context AND have 'org:manage_members' AND 'org:view_analytics' permissions
```

#### Parameter Decorators

Extractors that inject authentication data into route handler parameters. These provide clean access to user context.

```typescript
import {
  CurrentUser,
  UserRoles,
  AuthInfo
} from '@science-and-humans/science-and-humans-auth-sdk';

getUser(@CurrentUser() user: ClerkUser)
// Inject authenticated user object with full profile data
// Returns null if user is not authenticated

getRoles(@UserRoles() roles: string[])
// Inject array of user's roles
// Returns empty array if no roles or not authenticated

getAuthContext(@AuthInfo() auth: AuthContext)
// Inject complete authentication context
// Includes user, roles, permissions, token info, and auth status

getUserRoleInfo(@GetUserRoleInfo() roleInfo: UserRoleInfo)
// Inject detailed role information including organization context
// Returns role context (user/organization), org role, permissions, and organization ID
```

### Middleware

#### AuthMiddleware

Core NestJS middleware that processes every request to extract and validate authentication tokens. Automatically populates request.auth with authentication context.

```typescript
import { AuthMiddleware } from '@science-and-humans/science-and-humans-auth-sdk';

// Automatically registered when using AuthMiddlewareModule
// Runs on every request to:
// 1. Extract JWT tokens from Authorization header
// 2. Validate tokens using AuthClient
// 3. Populate request.auth with user context
// 4. Handle authentication errors gracefully
```

#### Express Support

Factory functions for integrating with Express applications. Provides the same authentication capabilities outside of NestJS.

```typescript
import {
  createAuthMiddleware,
  requireAuth,
} from '@science-and-humans/science-and-humans-auth-sdk';

const app = express();
const authClient = new AuthClient(config);

// Apply auth middleware - Processes all requests
app.use(createAuthMiddleware(authClient));
// Extracts tokens and populates req.auth for all routes

// Protect specific routes - Declarative authentication
app.get('/protected', requireAuth(), (req, res) => {
  res.json({ user: req.auth?.user });
});
// Ensures user is authenticated, throws 401 if not

// Role-based protection - Fine-grained authorization
app.get('/admin', requireAuth({ roles: ['admin'] }), (req, res) => {
  res.json({ message: 'Admin only' });
});
// Requires both authentication AND admin role
```

### Exception Filters

#### AuthExceptionFilter

Specialized exception filter that transforms authentication errors into proper HTTP responses. Provides consistent error formatting and logging.

```typescript
import { AuthExceptionFilter } from '@science-and-humans/science-and-humans-auth-sdk';

@UseFilters(AuthExceptionFilter)
@Controller()
export class MyController {}
// Catches authentication-related exceptions and formats them properly
// Converts internal auth errors to appropriate HTTP status codes (401, 403)
// Provides detailed error messages while protecting sensitive information
```

#### GlobalAuthAwareExceptionFilter

Enhanced global exception filter that understands authentication context. Provides better error responses based on user's auth status.

```typescript
import { GlobalAuthAwareExceptionFilter } from '@science-and-humans/science-and-humans-auth-sdk';

// In main.ts
app.useGlobalFilters(new GlobalAuthAwareExceptionFilter());
// Handles all exceptions with awareness of user authentication state
// Provides different error responses for authenticated vs anonymous users
// Logs security-relevant exceptions for monitoring and debugging
```

### Health Monitoring

#### RedisHealthService

Dedicated service for monitoring Redis connection health and performance. Provides real-time status checks and connection management.

```typescript
import { RedisHealthService } from '@science-and-humans/science-and-humans-auth-sdk';

const healthService = new RedisHealthService({
  host: 'localhost',
  port: 6379,
});

// Quick health check - Returns boolean status
const isHealthy = await healthService.isHealthy();
// true if Redis is reachable and responsive

// Detailed health status - Returns comprehensive health information
const status = await healthService.getHealthStatus();
// Returns: { status: 'healthy'|'degraded'|'unhealthy', lastCheck: Date, responseTime: number }

// Automatically handles:
// - Connection pooling and cleanup
// - Timeout management
// - Retry logic for transient failures
// - Performance monitoring
```

## Organization Authorization

The SDK provides advanced organization-level authorization capabilities that extend beyond basic user authentication. This feature allows you to manage complex multi-tenant applications with organization-specific roles and permissions.

### Organization Context

Organization authorization operates in a distinct context from user-level authorization. When a user is part of an organization, they can have:

- **Organization Roles**: Roles specific to their position within the organization (e.g., `org:admin`, `org:member`, `org:moderator`)
- **Organization Permissions**: Fine-grained permissions for organization-specific actions (e.g., `org:manage_members`, `org:view_analytics`, `org:edit_settings`)

### Organization Authorization Flow

```typescript
import { Controller, Get, UseGuards } from '@nestjs/common';
import {
  AuthGuard,
  RolesGuard,
  RequireOrgRole,
  RequireOrgPermission,
  CurrentUser,
  GetUserRoleInfo,
  UserRoleInfo,
} from '@science-and-humans/science-and-humans-auth-sdk';

@Controller('organization')
@UseGuards(AuthGuard, RolesGuard)
export class OrganizationController {
  @Get('dashboard')
  @RequireOrgRole('org:admin', 'org:moderator')
  getOrgDashboard(
    @CurrentUser() user: ClerkUser,
    @GetUserRoleInfo() roleInfo: UserRoleInfo
  ) {
    return {
      message: 'Organization dashboard',
      user: user.id,
      organizationId: roleInfo.organizationId,
      orgRole: roleInfo.orgRole,
      context: roleInfo.context,
    };
  }

  @Get('members')
  @RequireOrgPermission('org:manage_members')
  getOrgMembers(@GetUserRoleInfo() roleInfo: UserRoleInfo) {
    return {
      message: 'Organization members list',
      organizationId: roleInfo.organizationId,
      permissions: roleInfo.permissions,
    };
  }

  @Get('analytics')
  @RequireOrgPermission('org:view_analytics', 'org:admin')
  getOrgAnalytics() {
    return { message: 'Organization analytics' };
  }
}
```

### Organization Authorization with AuthClient

The `AuthClient` supports organization context in all authorization operations:

```typescript
import { AuthClient } from '@science-and-humans/science-and-humans-auth-sdk';

const authClient = new AuthClient({
  authServiceUrl: 'http://localhost:3000',
  organizationId: 'org_30yWUKhkqRl4aexwv3aJelcoRbT', // Default org context
});

// Validate token with organization context
const validation = await authClient.validateToken({
  token: 'jwt-token',
  organizationId: 'org_30yWUKhkqRl4aexwv3aJelcoRbT',
  context: 'organization',
});

// Authorize user for organization-specific actions
const auth = await authClient.authorizeUser({
  userId: 'user_123',
  resource: 'organization_members',
  action: 'manage',
  organizationId: 'org_30yWUKhkqRl4aexwv3aJelcoRbT',
  context: 'organization',
});
```

### Organization Types

```typescript
import {
  ClerkOrganization,
  OrganizationRole,
  OrganizationPermission,
  UserRoleInfo,
} from '@science-and-humans/science-and-humans-auth-sdk';

interface UserRoleInfo {
  context: 'user' | 'organization';
  organizationId?: string;
  orgRole?: string;
  permissions?: string[];
}

interface ClerkOrganization {
  id: string;
  name: string;
  slug?: string;
  imageUrl: string;
  hasImage: boolean;
  membersCount?: number;
  maxAllowedMemberships: number;
  adminDeleteEnabled: boolean;
  publicMetadata: Record<string, unknown>;
  privateMetadata?: Record<string, unknown>;
  createdAt: Date;
  updatedAt: Date;
}

interface OrganizationRole {
  id: string;
  key: string;
  name: string;
  description?: string;
  permissions: OrganizationPermission[];
  createdAt: Date;
  updatedAt: Date;
}

interface OrganizationPermission {
  id: string;
  key: string;
  name: string;
  description?: string;
  type: 'system' | 'custom';
  createdAt: Date;
  updatedAt: Date;
}
```

### Best Practices for Organization Authorization

1. **Context Awareness**: Always check if the user is in the correct context (user vs organization)
2. **Role Hierarchy**: Design organization roles with clear hierarchies (admin > moderator > member)
3. **Permission Granularity**: Use specific permissions for fine-grained access control
4. **Default Organization**: Set a default organization ID in your configuration for consistent behavior
5. **Error Handling**: Handle organization context errors gracefully with appropriate user feedback

## Type Definitions

### Core Types

```typescript
import {
  ClerkUser,
  ClerkSession,
  AuthConfig,
  AuthValidationRequest,
  AuthValidationResponse,
  User,
  Role,
  Permission,
  ClerkOrganization,
  OrganizationRole,
  OrganizationPermission,
  UserRoleInfo,
} from '@science-and-humans/science-and-humans-auth-sdk';
```

### Configuration Types

```typescript
interface AuthConfig {
  authServiceUrl: string;
  serviceKey?: string;
  serviceName?: string;
  organizationId?: string; // Default organization context for all requests
  requestTimeout?: number;
  redis: {
    host: string;
    port: number;
    password?: string;
    db?: number;
  };
}
```

### User Types

```typescript
interface User {
  id: string;
  email: string;
  firstName?: string;
  lastName?: string;
  role: string;
  isVerified: boolean;
  createdAt: Date;
  updatedAt: Date;
  metadata?: Record<string, any>;
}

interface Role {
  id: string;
  name: string;
  permissions: Permission[];
  description?: string;
}

interface Permission {
  id: string;
  name: string;
  resource: string;
  action: string;
  description?: string;
}
```

## Configuration Examples

### Environment-based Configuration

```typescript
AuthMiddlewareModule.forRoot({
  authServiceUrl: process.env.AUTH_SERVICE_URL || 'http://localhost:3000',
  serviceKey: process.env.AUTH_SERVICE_KEY,
  serviceName: process.env.SERVICE_NAME || 'my-service',
  organizationId: process.env.ORGANIZATION_ID, // Default organization context
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379'),
    password: process.env.REDIS_PASSWORD,
    db: parseInt(process.env.REDIS_DB || '0'),
  },
  requestTimeout: parseInt(process.env.AUTH_TIMEOUT || '5000'),
});
```

### Async Configuration with ConfigService

```typescript
AuthMiddlewareModule.forRootAsync({
  inject: [ConfigService],
  useFactory: async (configService: ConfigService) => ({
    authServiceUrl: configService.get('auth.serviceUrl'),
    serviceKey: configService.get('auth.serviceKey'),
    serviceName: configService.get('app.name'),
    redis: {
      host: configService.get('redis.host'),
      port: configService.get('redis.port'),
      password: configService.get('redis.password'),
      db: configService.get('redis.db'),
    },
  }),
});
```

## Error Handling

The middleware distinguishes between different types of errors:

- **Authentication Errors**: Invalid/expired tokens → Returns 401 immediately
- **Infrastructure Errors**: Redis/network issues → Triggers HTTP fallback
- **Authorization Errors**: Insufficient permissions → Returns 403

```typescript
try {
  // Protected operation
} catch (error) {
  if (error instanceof UnauthorizedException) {
    // Handle authentication failure
  } else if (error instanceof ForbiddenException) {
    // Handle authorization failure
  }
}
```

## Health Monitoring

```typescript
// Check overall health
const health = await authClient.healthCheck();
console.log(health);
// { status: 'healthy', services: { redis: 'up', http: 'up' } }

// Monitor Redis specifically
const redisHealth = new RedisHealthService(redisConfig);
const isHealthy = await redisHealth.isHealthy();
```

## Best Practices

1. **Always use environment variables** for sensitive configuration
2. **Combine AuthGuard with RolesGuard** for role-based access control
3. **Use @Public() decorator** to explicitly mark public endpoints
4. **Monitor health status** in production environments
5. **Handle errors gracefully** with proper exception filters

## License

MIT License