UnlockOS Developers
← Back to blog
🔐

JWT Security Architecture for Smart Lock Systems

Feb 9, 2026Feb 15, 2026
6 min
61 commits
Depth 8/10
securityauthenticationjwtapi-security

JWT Security Architecture for Smart Lock Systems

Introduction

In security-critical systems like smart lock management, authentication architecture decisions can make or break system trust. Recent security hardening efforts in our UnlockOS SDK revealed important patterns about JWT verification strategies and defense-in-depth approaches that every developer building IoT access control systems should understand.

The JWT Verification Dilemma

When building distributed systems with edge functions, you face a critical decision: rely on platform-level JWT verification or implement internal authentication validation. Our analysis revealed that blind trust in external JWT verification can create stability issues in production.

Platform JWT vs Internal Validation

Initially, our edge functions relied on platform JWT verification:

// Initial approach - platform dependency
export const config = {
  verify_jwt: true  // Platform handles JWT verification
}

export default async function handler(req: Request) {
  // Trust that JWT is already validated
  const userId = req.headers.get('user-id')
  return processLockAccess(userId)
}

This approach created several vulnerabilities:

  • Platform JWT verification could fail silently
  • No control over JWT validation logic
  • Difficult to audit authentication flows
  • Limited error handling capabilities

Defense-in-Depth Authentication

The solution was implementing internal authentication validation while disabling platform JWT verification:

// Hardened approach - internal validation
export const config = {
  verify_jwt: false  // Handle JWT validation internally
}

interface AuthContext {
  userId: string
  propertyId: string
  permissions: string[]
  expiresAt: number
}

export default async function handler(req: Request) {
  try {
    const authHeader = req.headers.get('Authorization')
    if (!authHeader?.startsWith('Bearer ')) {
      return new Response('Unauthorized', { status: 401 })
    }
    
    const token = authHeader.substring(7)
    const authContext = await validateInternalAuth(token)
    
    if (!authContext) {
      return new Response('Invalid token', { status: 401 })
    }
    
    return await processLockAccess(authContext)
  } catch (error) {
    logSecurityEvent('auth_validation_failed', { error })
    return new Response('Authentication failed', { status: 401 })
  }
}

async function validateInternalAuth(token: string): Promise<AuthContext | null> {
  // Custom JWT validation with proper error handling
  const decoded = await verifyJWT(token, process.env.JWT_SECRET!)
  
  if (decoded.exp < Date.now() / 1000) {
    throw new Error('Token expired')
  }
  
  // Additional business logic validation
  const user = await validateUserAccess(decoded.userId)
  if (!user.isActive) {
    throw new Error('User account inactive')
  }
  
  return {
    userId: decoded.userId,
    propertyId: decoded.propertyId,
    permissions: user.permissions,
    expiresAt: decoded.exp
  }
}

State-Aware Key Management

Smart lock systems require careful state management to prevent security vulnerabilities. Our implementation demonstrates several critical patterns.

Key Lifecycle State Machine

Managing digital keys requires explicit state tracking to prevent unauthorized access:

type KeyState = 
  | 'pending'
  | 'active'
  | 'expired'
  | 'revoked'
  | 'auto_checkout_pending'

interface LockKey {
  id: string
  state: KeyState
  issuedAt: Date
  expiresAt: Date
  lastRefreshed?: Date
  refreshCount: number
}

class KeyStateManager {
  async transitionKeyState(
    keyId: string, 
    fromState: KeyState, 
    toState: KeyState,
    context: AuthContext
  ): Promise<boolean> {
    // Validate state transition
    if (!this.isValidTransition(fromState, toState)) {
      logSecurityEvent('invalid_key_transition', {
        keyId, fromState, toState, userId: context.userId
      })
      return false
    }
    
    // Atomic state update with audit trail
    const success = await this.updateKeyState(keyId, toState, {
      previousState: fromState,
      updatedBy: context.userId,
      timestamp: new Date(),
      reason: this.getTransitionReason(fromState, toState)
    })
    
    if (success) {
      await this.notifyStateChange(keyId, fromState, toState)
    }
    
    return success
  }
  
  private isValidTransition(from: KeyState, to: KeyState): boolean {
    const validTransitions: Record<KeyState, KeyState[]> = {
      'pending': ['active', 'revoked'],
      'active': ['expired', 'revoked', 'auto_checkout_pending'],
      'expired': ['revoked'],
      'revoked': [], // Terminal state
      'auto_checkout_pending': ['expired', 'revoked']
    }
    
    return validTransitions[from]?.includes(to) ?? false
  }
}

Preventing Infinite Refresh Loops

A critical security issue emerged with infinite key refresh loops that could overwhelm the system:

class SecureKeyRefreshManager {
  private static readonly MAX_REFRESH_INTERVAL = 24 * 60 * 60 * 1000 // 24 hours
  private static readonly MIN_REFRESH_INTERVAL = 5 * 60 * 1000 // 5 minutes
  
  async refreshKeyIfNeeded(keyId: string, currentState: KeyState): Promise<RefreshResult> {
    // Prevent refresh for terminal states
    if (currentState === 'revoked' || currentState === 'expired') {
      return { success: false, reason: 'key_in_terminal_state' }
    }
    
    const key = await this.getKey(keyId)
    if (!key) {
      return { success: false, reason: 'key_not_found' }
    }
    
    // Rate limiting based on last refresh
    if (key.lastRefreshed) {
      const timeSinceRefresh = Date.now() - key.lastRefreshed.getTime()
      if (timeSinceRefresh < SecureKeyRefreshManager.MIN_REFRESH_INTERVAL) {
        return { success: false, reason: 'rate_limited' }
      }
    }
    
    // 24-hour guard for duration-based keys
    if (key.type === 'duration_flat') {
      const timeSinceIssue = Date.now() - key.issuedAt.getTime()
      if (timeSinceIssue < SecureKeyRefreshManager.MAX_REFRESH_INTERVAL) {
        return { success: false, reason: '24h_guard_active' }
      }
    }
    
    // Use Page Visibility API to prevent background refreshes
    if (typeof document !== 'undefined' && document.hidden) {
      return { success: false, reason: 'page_not_visible' }
    }
    
    return await this.performKeyRefresh(keyId)
  }
  
  private async performKeyRefresh(keyId: string): Promise<RefreshResult> {
    try {
      const refreshedKey = await this.keyService.refreshKey(keyId)
      
      // Update refresh tracking
      await this.updateRefreshMetrics(keyId, {
        lastRefreshed: new Date(),
        refreshCount: (await this.getKey(keyId))!.refreshCount + 1
      })
      
      logSecurityEvent('key_refreshed', { keyId })
      
      return { success: true, key: refreshedKey }
    } catch (error) {
      logSecurityEvent('key_refresh_failed', { keyId, error: error.message })
      return { success: false, reason: 'refresh_failed' }
    }
  }
}

Robust Error Handling Patterns

Graceful State Recovery

Smart lock systems must handle network failures and state inconsistencies gracefully:

class CheckinStateRecovery {
  async persistStateForRecovery(checkinData: CheckinState): Promise<void> {
    // Persist critical state to localStorage for recovery
    const recoveryData = {
      entryKey: checkinData.entryKey,
      propertyId: checkinData.propertyId,
      checkInTime: checkinData.checkInTime.toISOString(),
      keyState: checkinData.keyState,
      timestamp: Date.now()
    }
    
    try {
      localStorage.setItem(
        `checkin_recovery_${checkinData.propertyId}`, 
        JSON.stringify(recoveryData)
      )
    } catch (error) {
      // Handle storage quota exceeded
      this.clearOldRecoveryData()
      localStorage.setItem(
        `checkin_recovery_${checkinData.propertyId}`, 
        JSON.stringify(recoveryData)
      )
    }
  }
  
  async recoverStateAfterReload(): Promise<CheckinState | null> {
    const recoveryKeys = Object.keys(localStorage)
      .filter(key => key.startsWith('checkin_recovery_'))
    
    for (const key of recoveryKeys) {
      try {
        const recoveryData = JSON.parse(localStorage.getItem(key)!)
        
        // Validate recovery data age (max 24 hours)
        if (Date.now() - recoveryData.timestamp > 24 * 60 * 60 * 1000) {
          localStorage.removeItem(key)
          continue
        }
        
        // Verify state with server
        const serverState = await this.verifyStateWithServer(recoveryData)
        if (serverState) {
          return this.reconstructState(recoveryData, serverState)
        }
        
      } catch (error) {
        logSecurityEvent('state_recovery_failed', { key, error })
        localStorage.removeItem(key) // Clean up corrupted data
      }
    }
    
    return null
  }
}

Summary

Building trust in smart lock systems requires implementing multiple layers of security controls. Key principles include:

  • Defense-in-Depth Authentication: Never rely solely on platform JWT verification; implement internal validation with comprehensive error handling
  • Explicit State Management: Use state machines to prevent invalid key transitions and maintain audit trails
  • Rate Limiting and Guards: Implement multiple safeguards against infinite loops and resource exhaustion
  • Graceful Recovery: Design systems to recover from failures while maintaining security boundaries

These patterns ensure that your smart lock system remains secure and reliable even when individual components fail, building the trust necessary for security-critical applications.

Key Insights

1
Security

Defense-in-Depth JWT Validation

Implementing internal JWT validation alongside platform verification provides better security control and audit capabilities

2
State Management

Key Lifecycle State Machines

Explicit state transitions with validation prevent unauthorized key usage and maintain comprehensive audit trails

3
Reliability

Anti-Pattern Protection

Rate limiting, 24-hour guards, and visibility-based throttling prevent infinite refresh loops and resource exhaustion

4
Error Handling

Stateful Recovery Mechanisms

Persistent state recovery with server verification enables graceful handling of network failures and page reloads