UnlockOS Developers
← 記事一覧に戻る
🔐

スマートロックシステムのJWTセキュリティアーキテクチャ

2026年2月9日2026年2月15日
6
61 commits
深度 8/10
securityauthenticationjwtapi-security

スマートロックシステムのJWTセキュリティアーキテクチャ

はじめに

スマートロック管理のようなセキュリティが重要なシステムにおいて、認証アーキテクチャの設計判断はシステムの信頼性を左右します。私たちのUnlockOS SDKにおける最近のセキュリティ強化の取り組みから、JWT検証戦略と多層防御アプローチに関する重要なパターンが明らかになりました。これらは、IoTアクセス制御システムを構築するすべての開発者が理解すべき内容です。

JWT検証のジレンマ

エッジ関数を使用した分散システムを構築する際、重要な決断を迫られます:プラットフォームレベルのJWT検証に依存するか、内部認証検証を実装するかです。私たちの分析では、外部JWT検証への盲目的な信頼は本番環境で安定性の問題を引き起こす可能性があることが判明しました。

プラットフォームJWT vs 内部検証

当初、私たちのエッジ関数はプラットフォームJWT検証に依存していました:

// 初期のアプローチ - プラットフォーム依存
export const config = {
  verify_jwt: true  // プラットフォームがJWT検証を処理
}

export default async function handler(req: Request) {
  // JWTが既に検証済みであることを信頼
  const userId = req.headers.get('user-id')
  return processLockAccess(userId)
}

このアプローチは複数の脆弱性を生み出しました:

  • プラットフォームJWT検証がサイレントに失敗する可能性
  • JWT検証ロジックの制御ができない
  • 認証フローの監査が困難
  • エラーハンドリング機能に制限

多層防御認証

解決策は、プラットフォームJWT検証を無効にしながら内部認証検証を実装することでした:

// 強化されたアプローチ - 内部検証
export const config = {
  verify_jwt: false  // JWT検証を内部で処理
}

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> {
  // 適切なエラーハンドリングを伴うカスタムJWT検証
  const decoded = await verifyJWT(token, process.env.JWT_SECRET!)
  
  if (decoded.exp < Date.now() / 1000) {
    throw new Error('Token expired')
  }
  
  // 追加のビジネスロジック検証
  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
  }
}

状態を意識したキー管理

スマートロックシステムではセキュリティ脆弱性を防ぐために慎重な状態管理が必要です。私たちの実装では、いくつかの重要なパターンを実証しています。

キーライフサイクル状態機械

デジタルキーの管理には、不正アクセスを防ぐための明示的な状態追跡が必要です:

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> {
    // 状態遷移の検証
    if (!this.isValidTransition(fromState, toState)) {
      logSecurityEvent('invalid_key_transition', {
        keyId, fromState, toState, userId: context.userId
      })
      return false
    }
    
    // 監査証跡を伴うアトミック状態更新
    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': [], // 終端状態
      'auto_checkout_pending': ['expired', 'revoked']
    }
    
    return validTransitions[from]?.includes(to) ?? false
  }
}

無限リフレッシュループの防止

システムを圧迫する可能性のある無限キーリフレッシュループという重要なセキュリティ問題が発生しました:

class SecureKeyRefreshManager {
  private static readonly MAX_REFRESH_INTERVAL = 24 * 60 * 60 * 1000 // 24時間
  private static readonly MIN_REFRESH_INTERVAL = 5 * 60 * 1000 // 5分
  
  async refreshKeyIfNeeded(keyId: string, currentState: KeyState): Promise<RefreshResult> {
    // 終端状態のリフレッシュを防止
    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' }
    }
    
    // 最後のリフレッシュに基づくレート制限
    if (key.lastRefreshed) {
      const timeSinceRefresh = Date.now() - key.lastRefreshed.getTime()
      if (timeSinceRefresh < SecureKeyRefreshManager.MIN_REFRESH_INTERVAL) {
        return { success: false, reason: 'rate_limited' }
      }
    }
    
    // 期間ベースキーの24時間ガード
    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' }
      }
    }
    
    // Page Visibility APIを使用してバックグラウンドリフレッシュを防止
    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)
      
      // リフレッシュ追跡の更新
      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' }
    }
  }
}

堅牢なエラーハンドリングパターン

優雅な状態復旧

スマートロックシステムは、ネットワーク障害や状態の不整合を優雅に処理する必要があります:

class CheckinStateRecovery {
  async persistStateForRecovery(checkinData: CheckinState): Promise<void> {
    // 復旧のための重要な状態をlocalStorageに永続化
    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) {
      // ストレージクォータ超過の処理
      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)!)
        
        // 復旧データの経過時間を検証(最大24時間)
        if (Date.now() - recoveryData.timestamp > 24 * 60 * 60 * 1000) {
          localStorage.removeItem(key)
          continue
        }
        
        // サーバーとの状態検証
        const serverState = await this.verifyStateWithServer(recoveryData)
        if (serverState) {
          return this.reconstructState(recoveryData, serverState)
        }
        
      } catch (error) {
        logSecurityEvent('state_recovery_failed', { key, error })
        localStorage.removeItem(key) // 破損したデータのクリーンアップ
      }
    }
    
    return null
  }
}

まとめ

スマートロックシステムで信頼性を構築するには、複数層のセキュリティ制御を実装する必要があります。主要な原則は以下の通りです:

  • 多層防御認証: プラットフォームJWT検証のみに依存せず、包括的なエラーハンドリングを伴う内部検証を実装
  • 明示的状態管理: 状態機械を使用して無効なキー遷移を防止し、監査証跡を維持
  • レート制限とガード: 無限ループとリソース枯渇に対する複数の保護策を実装
  • 優雅な復旧: セキュリティ境界を維持しながら障害から復旧するようシステムを設計

これらのパターンにより、個々のコンポーネントが失敗してもスマートロックシステムが安全で信頼性を保ち、セキュリティが重要なアプリケーションに必要な信頼を構築します。

主要な洞察

[セクション内容は以下のkeyInsights配列に移動]

主要な発見

1
セキュリティ

多層防御JWT検証

プラットフォーム検証と併用した内部JWT検証により、より良いセキュリティ制御と監査機能を提供

2
状態管理

キーライフサイクル状態機械

検証を伴う明示的な状態遷移により、不正なキー使用を防止し包括的な監査証跡を維持

3
信頼性

アンチパターン保護

レート制限、24時間ガード、可視性ベーススロットリングにより無限リフレッシュループとリソース枯渇を防止

4
エラーハンドリング

ステートフル復旧メカニズム

サーバー検証を伴う永続状態復旧により、ネットワーク障害とページリロードの優雅な処理を実現