UnlockOS Developers
← Back to blog
🔐

Building Secure Access Control with Type Safety & State Validation

Mar 30, 2026Apr 5, 2026
7 min
253 commits
Depth 8/10
securitytypescriptvalidationstate-machineaccess-control

Building Secure Access Control with Type Safety & State Validation

Introduction

In security-critical systems managing physical access, every line of code becomes a potential attack vector. This analysis examines how proper type safety, input validation, and state management create layers of defense that protect both data integrity and user safety.

Critical Security Hardening Patterns

Input Validation and XSS Prevention

One of the most dangerous vulnerabilities in access control systems is Cross-Site Scripting (XSS) in error displays. The commits reveal a critical fix:

// VULNERABLE: Direct user input rendering
const ErrorDisplay = ({ configId }: { configId: string }) => {
  return <div>Config not found: {configId}</div>; // XSS risk
};

// SECURE: Sanitized rendering with validation
const ErrorDisplay = ({ configId }: { configId: string }) => {
  const sanitizedId = configId.replace(/[<>"'&]/g, '');
  const isValidId = /^[a-zA-Z0-9-_]{1,50}$/.test(configId);
  
  if (!isValidId) {
    return <div>Invalid configuration identifier</div>;
  }
  
  return <div>Config not found: {sanitizedId}</div>;
};

RBAC Authorization with Multi-Layer Validation

The implementation demonstrates a sophisticated Role-Based Access Control (RBAC) system across three validation layers:

// Layer 1: JWT Claims Validation
interface JWTClaims {
  sub: string;
  role: 'platform_admin' | 'facility_admin' | 'guest';
  facility_id?: string;
  organization_id: string;
}

// Layer 2: Database-Level RLS Policy
const createRLSPolicy = () => `
  CREATE POLICY facility_access ON reservations
  FOR SELECT USING (
    facility_id = auth.jwt() ->> 'facility_id'
    OR auth.jwt() ->> 'role' = 'platform_admin'
  );
`;

// Layer 3: Application Logic Validation
const validateFacilityAccess = async (userId: string, facilityId: string) => {
  const userRoles = await getUserRoles(userId);
  
  if (userRoles.includes('platform_admin')) {
    return true; // Platform admin has global access
  }
  
  return userRoles.some(role => 
    role.facility_id === facilityId && 
    ['facility_admin', 'staff'].includes(role.role_name)
  );
};

Fail-Safe Security Patterns

The codebase eliminates dangerous "fail-open" patterns that could grant unauthorized access:

// DANGEROUS: Fail-open pattern
const getConfigDangerous = async (slug: string) => {
  try {
    return await fetchConfig(slug);
  } catch (error) {
    return getDefaultConfig(); // SECURITY RISK: Falls back to default
  }
};

// SECURE: Fail-closed pattern
const getConfigSecure = async (slug: string) => {
  const config = await fetchConfig(slug);
  
  if (!config) {
    throw new Error(`Configuration not found: ${slug}`);
  }
  
  // Validate config completeness
  const requiredFields = ['apiKey', 'facilityId', 'permissions'];
  for (const field of requiredFields) {
    if (!config[field]) {
      throw new Error(`Invalid configuration: missing ${field}`);
    }
  }
  
  return config;
};

State Machine Reliability

Reservation State Management

The system implements a strict finite state machine for reservation lifecycle management:

type ReservationStatus = 
  | 'pending'
  | 'confirmed' 
  | 'checked_in'
  | 'completed'
  | 'cancelled';

interface ReservationState {
  status: ReservationStatus;
  checkInTime?: Date;
  keyIssued?: boolean;
  paymentStatus: 'pending' | 'paid' | 'failed';
}

const validateStateTransition = (
  currentStatus: ReservationStatus, 
  newStatus: ReservationStatus
): boolean => {
  const validTransitions: Record<ReservationStatus, ReservationStatus[]> = {
    pending: ['confirmed', 'cancelled'],
    confirmed: ['checked_in', 'cancelled'],
    checked_in: ['completed'],
    completed: [], // Terminal state
    cancelled: []  // Terminal state
  };
  
  return validTransitions[currentStatus]?.includes(newStatus) ?? false;
};

Lock State Coordination

For physical lock management, the system prevents race conditions and ensures atomic operations:

interface LockOperation {
  lockId: string;
  operation: 'issue_key' | 'revoke_key' | 'extend_access';
  userId: string;
  expiresAt: Date;
}

const executeLockOperation = async (operation: LockOperation) => {
  // Use advisory locks to prevent concurrent operations
  const lockAcquired = await acquireAdvisoryLock(
    `lock_${operation.lockId}`,
    30000 // 30 second timeout
  );
  
  if (!lockAcquired) {
    throw new Error('Lock operation timeout - device may be busy');
  }
  
  try {
    // Validate current lock state
    const currentState = await getLockState(operation.lockId);
    
    if (currentState.maintenance_mode) {
      throw new Error('Lock is in maintenance mode');
    }
    
    // Execute operation with rollback capability
    const result = await performLockOperation(operation);
    
    // Log for audit trail
    await logSecurityEvent({
      type: 'lock_operation',
      lockId: operation.lockId,
      userId: operation.userId,
      operation: operation.operation,
      result: result.success ? 'success' : 'failure',
      timestamp: new Date(),
      metadata: { pin: result.pin, expiresAt: operation.expiresAt }
    });
    
    return result;
  } finally {
    await releaseAdvisoryLock(`lock_${operation.lockId}`);
  }
};

Comprehensive Error Handling

Graceful Degradation Patterns

The system implements graceful degradation for external service failures:

const issueKeyWithFallback = async (reservationId: string) => {
  try {
    // Primary: Hardware key issuance
    const hardwareKey = await issueHardwareKey(reservationId);
    return { type: 'hardware', key: hardwareKey };
  } catch (hardwareError) {
    console.warn('Hardware key issuance failed:', hardwareError);
    
    try {
      // Fallback: Digital PIN
      const digitalPin = await issueDigitalPin(reservationId);
      return { type: 'digital', key: digitalPin };
    } catch (digitalError) {
      // Log critical failure for investigation
      await logCriticalEvent({
        type: 'key_issuance_failure',
        reservationId,
        errors: [hardwareError, digitalError],
        timestamp: new Date()
      });
      
      throw new Error('Unable to issue access credentials');
    }
  }
};

Webhook Security and Idempotency

Webhook processing implements idempotency and signature verification:

const processWebhook = async (payload: WebhookPayload, signature: string) => {
  // Verify webhook signature
  const expectedSignature = createHmac('sha256', WEBHOOK_SECRET)
    .update(JSON.stringify(payload))
    .digest('hex');
    
  if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
    throw new Error('Invalid webhook signature');
  }
  
  // Idempotency check
  const existingEvent = await findWebhookEvent(payload.id);
  if (existingEvent) {
    return existingEvent.result; // Already processed
  }
  
  // Process with transaction rollback on failure
  return await processInTransaction(async (tx) => {
    const result = await handleWebhookEvent(payload, tx);
    
    // Record successful processing
    await tx.insert('webhook_events', {
      id: payload.id,
      type: payload.type,
      processed_at: new Date(),
      result: result
    });
    
    return result;
  });
};

Testing Strategy for Security

Property-Based Security Tests

The implementation includes comprehensive security-focused tests:

// Test suite for access control validation
describe('Access Control Security', () => {
  test('should prevent privilege escalation', async () => {
    const guestUser = await createTestUser('guest');
    const adminFacility = await createTestFacility();
    
    // Attempt to access admin-only endpoint
    const response = await request(app)
      .get(`/api/facilities/${adminFacility.id}/admin`)
      .set('Authorization', `Bearer ${guestUser.token}`);
      
    expect(response.status).toBe(403);
    expect(response.body.error).toContain('Insufficient permissions');
  });
  
  test('should validate all input parameters', async () => {
    const maliciousInputs = [
      '<script>alert("xss")</script>',
      '../../etc/passwd',
      'SELECT * FROM users;',
      '${process.env.SECRET}'
    ];
    
    for (const input of maliciousInputs) {
      const response = await request(app)
        .post('/api/reservations')
        .send({ guestName: input });
        
      expect(response.status).toBe(400);
      expect(response.body.error).toContain('Invalid input');
    }
  });
});

Audit Logging and Compliance

Comprehensive Security Event Logging

interface SecurityEvent {
  eventId: string;
  eventType: 'authentication' | 'authorization' | 'access_granted' | 'access_denied';
  userId?: string;
  facilityId: string;
  ipAddress: string;
  userAgent: string;
  timestamp: Date;
  success: boolean;
  metadata: Record<string, any>;
}

const logSecurityEvent = async (event: Omit<SecurityEvent, 'eventId' | 'timestamp'>) => {
  const securityEvent: SecurityEvent = {
    ...event,
    eventId: generateUUID(),
    timestamp: new Date(),
  };
  
  // Write to secure audit log
  await writeToAuditLog(securityEvent);
  
  // Alert on suspicious patterns
  if (!event.success) {
    await checkForSuspiciousActivity(event.userId, event.ipAddress);
  }
};

Summary

Secure access control systems require multiple layers of defense working in harmony. The patterns demonstrated here show how type safety, input validation, state machine design, and comprehensive error handling create a robust foundation for managing physical access.

Key principles include:

  • Fail-closed security: Never fall back to permissive defaults
  • Multi-layer validation: Validate at JWT, database, and application levels
  • State machine integrity: Enforce valid state transitions
  • Comprehensive logging: Maintain audit trails for compliance and investigation
  • Graceful degradation: Handle failures without compromising security

These patterns are applicable to any system where security and reliability are paramount, providing a blueprint for building trustworthy access control infrastructure.

Key Insights

1
Security

Multi-Layer RBAC Validation

Implements defense-in-depth with JWT claims, database RLS policies, and application-level authorization checks

2
Security

Fail-Closed Security Patterns

Eliminates dangerous fail-open patterns that could grant unauthorized access during error conditions

3
State Management

Finite State Machine Design

Uses strict state transitions and advisory locks to prevent race conditions in lock operations

4
Reliability

Graceful Degradation with Security

Implements fallback mechanisms for key issuance while maintaining security guarantees

5
Validation

Comprehensive Input Sanitization

Prevents XSS and injection attacks through systematic input validation and sanitization