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.