17  Lab: Basic Access Control Setup

Components and Circuit Design for IoT Security

17.1 Learning Objectives

By completing this lab setup, you will be able to:

  • Identify the hardware components needed for an IoT access control system
  • Wire the circuit correctly for LEDs, buzzer, and buttons
  • Understand the code structure for authentication and authorization
  • Configure access levels for different user roles
  • Implement security features like account lockout and constant-time comparison

Access control determines what each user or device is allowed to do in an IoT system. Think of a hospital where doctors, nurses, and visitors each have different access levels – doctors can prescribe medication, nurses can administer it, and visitors can only visit patients. Similarly, IoT access control ensures each device and user can only perform actions appropriate to their role.

Prerequisites

Before starting this lab, you should:


Concept Relationships
Concept Related To Relationship Type
Access Levels RBAC, Hierarchy Implements - Guest < User < Admin follows role-based access control
Credential Database Authentication, Storage Stores - User identities and access levels for verification
Security State Session Management Tracks - Failed attempts, lockout status, current authentication state
Constant-Time Compare Side-Channel Defense Prevents - Timing attacks that leak password information via execution time
Audit Log Compliance, Forensics Records - All access events for security investigation and regulatory requirements
Debouncing Hardware Reliability Ensures - Single button press registers once, preventing double-authentication
See Also

Foundation Concepts:

Related Labs:

Security Foundations:


17.2 Components

Component Purpose Wokwi Element
ESP32 DevKit Main controller with access control logic esp32:devkit-v1
Green LED Access granted indicator led:green
Red LED Access denied indicator led:red
Yellow LED System status / rate limit warning led:yellow
Blue LED Admin mode indicator led:blue
Buzzer Audio feedback for access events buzzer
Push Button 1 Simulate RFID card tap (cycle through cards) button
Push Button 2 Request access with current card button
Resistors (4x 220 ohm) Current limiting for LEDs resistor
Try It: GPIO Pin Assignment Explorer

Explore how each ESP32 GPIO pin connects to a specific component. Select a component to see its pin, purpose, and wiring details.


17.3 Interactive Wokwi Simulator

Use the embedded simulator below to build and test your secure IoT access control system. Click “Start Simulation” after entering the code.


17.4 Circuit Connections

Before entering the code, wire the circuit in Wokwi:

ESP32 Pin Connections:
---------------------
GPIO 2  --> Green LED (+)  --> 220 ohm Resistor --> GND  (Access Granted)
GPIO 4  --> Red LED (+)    --> 220 ohm Resistor --> GND  (Access Denied)
GPIO 5  --> Yellow LED (+) --> 220 ohm Resistor --> GND  (System Status)
GPIO 18 --> Blue LED (+)   --> 220 ohm Resistor --> GND  (Admin Mode)
GPIO 19 --> Buzzer (+)     --> GND
GPIO 15 --> Button 1       --> GND  (Select RFID Card)
GPIO 16 --> Button 2       --> GND  (Request Access)
Circuit wiring diagram for an ESP32-based IoT access control system showing GPIO pin connections to four colored LEDs (green for access granted, red for access denied, yellow for system status, blue for admin mode) through 220 ohm current-limiting resistors, a buzzer for audio feedback, and two push buttons for RFID card selection and access requests
Figure 17.1: Circuit diagram showing ESP32 connections to LEDs, buzzer, and buttons

17.5 Code Structure Overview

The access control system is organized into several key sections:

17.5.1 1. Pin Definitions and Access Levels

/*
 * Secure IoT Access Control System
 * Demonstrates: RFID-style authentication, role-based access control,
 *               lockout policies, and comprehensive audit logging
 *
 * Security Concepts Implemented:
 * 1. Token-based authentication (simulating RFID cards)
 * 2. Role-based access control (RBAC) with three access levels
 * 3. Account lockout after failed attempts
 * 4. Comprehensive audit logging with timestamps
 * 5. Visual and audio feedback for security events
 * 6. Constant-time comparison to prevent timing attacks
 */

#include <Arduino.h>

// ============== PIN DEFINITIONS ==============
const int LED_ACCESS_GRANTED = 2;   // Green LED
const int LED_ACCESS_DENIED = 4;    // Red LED
const int LED_SYSTEM_STATUS = 5;    // Yellow LED
const int LED_ADMIN_MODE = 18;      // Blue LED
const int BUZZER_PIN = 19;          // Buzzer for audio feedback
const int BUTTON_SELECT = 15;       // Select RFID card
const int BUTTON_ACCESS = 16;       // Request access

// ============== ACCESS LEVELS ==============
enum AccessLevel {
    ACCESS_NONE = 0,
    ACCESS_GUEST = 1,
    ACCESS_USER = 2,
    ACCESS_ADMIN = 3
};

17.5.2 2. User Credential Structure

// ============== USER CREDENTIAL STRUCTURE ==============
struct UserCredential {
    const char* cardId;       // Simulated RFID card ID
    const char* userName;     // User name for logging
    AccessLevel accessLevel;  // Permission level
    bool isActive;            // Account status
};

// ============== CREDENTIAL DATABASE ==============
// In production: Store in secure element, never in code!
const int NUM_USERS = 6;
UserCredential userDatabase[NUM_USERS] = {
    {"RFID_ADMIN_001", "Alice Admin", ACCESS_ADMIN, true},
    {"RFID_ADMIN_002", "Bob Admin", ACCESS_ADMIN, true},
    {"RFID_USER_001", "Charlie User", ACCESS_USER, true},
    {"RFID_USER_002", "Diana User", ACCESS_USER, true},
    {"RFID_GUEST_001", "Eve Guest", ACCESS_GUEST, true},
    {"RFID_DISABLED", "Frank Former", ACCESS_USER, false}  // Disabled account
};

// Test cards for simulation (includes invalid cards)
const int NUM_TEST_CARDS = 8;
const char* testCards[NUM_TEST_CARDS] = {
    "RFID_ADMIN_001",   // Valid admin
    "RFID_USER_001",    // Valid user
    "RFID_GUEST_001",   // Valid guest
    "RFID_DISABLED",    // Disabled account
    "RFID_UNKNOWN_1",   // Unknown card
    "RFID_UNKNOWN_2",   // Unknown card
    "RFID_ADMIN_002",   // Valid admin
    "RFID_USER_002"     // Valid user
};

17.5.3 3. Security Configuration

// ============== SECURITY CONFIGURATION ==============
const int MAX_FAILED_ATTEMPTS = 3;
const unsigned long LOCKOUT_DURATION_MS = 60000;  // 1 minute lockout
const unsigned long LOCKOUT_ESCALATION_MS = 30000; // Add 30s per additional failure
const int MAX_LOCKOUT_DURATION_MS = 300000;  // Max 5 minutes

Brute Force Attack Prevention with Lockout Policies:

Consider an ESP32 access control system where an attacker attempts to present forged RFID tokens. The system has 6 valid tokens, and the attacker is trying unknown card IDs at random.

Without Lockout Protection: \[ \text{Token attempt rate} = 1 \text{ attempt per 2 seconds} = 0.5 \text{ Hz} \]

If the attacker can make unlimited guesses without penalty, they could systematically try card IDs indefinitely until finding a valid one.

With Linear Escalating Lockout (as configured in this lab):

The lab’s lockout policy works as follows:

  • After 3 consecutive failures: locked for 60 seconds (base)
  • Each additional failure adds 30 seconds
  • Maximum lockout: 300 seconds (5 minutes)

After every 3 failed attempts, the attacker must wait: \[ \text{Lockout}(n) = \min(60{,}000 + (n - 3) \times 30{,}000,\ 300{,}000) \text{ ms} \]

where \(n\) is the total number of consecutive failures.

Failures Lockout Duration Cumulative Wait
3 60 s 60 s
4 90 s 150 s
5 120 s 270 s
6 150 s 420 s
10 270 s 1,380 s
12+ 300 s (max) grows by 300 s per attempt

Once the lockout reaches the 300-second cap, the attacker can only make roughly 1 attempt every 5 minutes. Over 24 hours, that limits them to about 288 attempts – a dramatic reduction from the 43,200 attempts possible without lockout (at 0.5 Hz).

Defense Factor: \[ \frac{43{,}200 \text{ attempts/day (no lockout)}}{288 \text{ attempts/day (with lockout)}} = 150\times \text{ slower attack} \]

This makes brute force against unknown RFID token IDs impractical for any reasonably sized token space.

17.5.4 4. Access Control Zones

// ============== ACCESS CONTROL ZONES ==============
// Different areas with different access requirements
struct AccessZone {
    const char* zoneName;
    AccessLevel requiredLevel;
};

const int NUM_ZONES = 4;
AccessZone accessZones[NUM_ZONES] = {
    {"Public Lobby", ACCESS_GUEST},
    {"Office Area", ACCESS_USER},
    {"Server Room", ACCESS_ADMIN},
    {"Control Center", ACCESS_ADMIN}
};
Try It: Role-Based Access Control Zone Checker

Select a user from the credential database and a zone to check whether access is granted or denied based on the RBAC hierarchy (Guest < User < Admin).

17.5.5 5. Security State and Audit Log

// ============== SECURITY STATE ==============
struct SecurityState {
    int failedAttempts;
    unsigned long lockoutEndTime;
    bool isLocked;
    int currentCardIndex;
    String lastAuthenticatedUser;
    AccessLevel currentAccessLevel;
    unsigned long sessionStartTime;
} secState;

// ============== AUDIT LOG ==============
struct AuditEntry {
    unsigned long timestamp;
    const char* cardId;
    const char* userName;
    const char* eventType;
    const char* zoneName;
    bool success;
    AccessLevel attemptedLevel;
};

const int MAX_AUDIT_ENTRIES = 50;
AuditEntry auditLog[MAX_AUDIT_ENTRIES];
int auditIndex = 0;

17.6 Constant-Time Comparison

A critical security feature is constant-time string comparison, which prevents timing attacks:

// Constant-time string comparison prevents timing side-channel attacks
bool constantTimeCompare(const char* a, const char* b) {
    size_t lenA = strlen(a);
    size_t lenB = strlen(b);

    // Always compare full length to prevent length-based timing
    size_t maxLen = (lenA > lenB) ? lenA : lenB;

    volatile int result = 0;
    for (size_t i = 0; i < maxLen; i++) {
        char charA = (i < lenA) ? a[i] : 0;
        char charB = (i < lenB) ? b[i] : 0;
        result |= charA ^ charB;
    }

    // Also check lengths match
    result |= (lenA != lenB);

    return (result == 0);
}
Try It: Timing Attack Simulator

See how standard string comparison leaks information through timing, and how constant-time comparison prevents it. Enter a guess to compare against a hidden password and observe the execution time difference.


17.7 Password Hashing Concepts

While this lab uses simple string comparison for demonstration, production systems use cryptographic hashing:

Try It: Password Hash Brute Force Calculator

Explore how password length, character set, and hashing algorithm cost affect the time required for a brute force attack on stolen password hashes.

Scenario: You’re deploying an access control system for a 500-person office building. Calculate appropriate lockout settings to minimize help desk calls while maintaining security.

Current baseline (no lockout implemented):

  • 12,000 login attempts/month
  • 600 failed logins/month (5% failure rate)
  • 23 suspected brute force attacks/month
  • $18,000/year in help desk costs (password resets)

Proposed lockout policy:

const int MAX_FAILED_ATTEMPTS = 3;
const unsigned long LOCKOUT_DURATION_MS = 60000;  // 1 minute

Impact calculation:

Failed login distribution (analyzed from logs):
  1 failure:  420 users (70% - simple typo, corrected immediately)
  2 failures: 120 users (20% - forgotten password, second attempt works)
  3 failures:  45 users (7.5% - will trigger lockout)
  4+ failures: 15 users (2.5% - brute force or major user confusion)

Lockout events per month:
  3-attempt lockout = 45 + 15 = 60 users/month

Cost analysis:
  Help desk calls: 60 x $25/call = $1,500/month
  Productivity loss: 60 users x 5 min avg = 300 min = $750/month (@ $150/hr avg wage)
  Total cost: $2,250/month = $27,000/year

Security benefit:
  Brute force attempts stopped after 3 tries (was unlimited)
  Estimated prevented breaches: 2/year x $50,000 avg = $100,000 saved

Net benefit: $100,000 - $27,000 = $73,000/year ROI

Optimized with escalating lockout:

// Escalating lockout: base + (extra failures * escalation), capped at max
unsigned long calculateLockout(int attempts) {
    return min(LOCKOUT_DURATION_MS + (attempts - MAX_FAILED_ATTEMPTS) * LOCKOUT_ESCALATION_MS,
               (unsigned long)MAX_LOCKOUT_DURATION_MS);
}

// Results with MAX_FAILED_ATTEMPTS=3, base=60s, escalation=30s, max=300s:
//   3 attempts: 60s lockout (most users recover quickly)
//   4 attempts: 90s lockout
//   5 attempts: 120s lockout
//   11+ attempts: 300s lockout (max cap reached)

// New help desk calls: 45 (down 25% - faster recovery for short lockouts)
// Annual cost: $20,250 (vs $27,000)
// Additional savings: $6,750/year
Component Option A (Basic) Option B (Enhanced) Option C (Production)
LEDs 4 single-color LEDs RGB LED module Addressable LED strip
Cost $2 $5 $15
Complexity 4 GPIO pins 3 GPIO pins (R/G/B) 1 GPIO pin (data line)
Use Case Lab learning Prototype Production deploy
Feedback Options 4 states (G/R/Y/B) 16M colors Animations, patterns
Buzzer Passive (tone) Active (fixed freq) Piezo + amplifier
Cost $0.50 $1 $8
Audio Quality Beeps only Single tone Musical notes
Buttons Basic tactile Debounced hardware Capacitive touch
Cost $0.20 each $0.50 each $3 each
Reliability Bouncing (software fix) No bouncing No mechanical wear

Recommendation for this lab: Option A (Basic). Total cost: $4.40. Focus is on learning authentication concepts, not hardware polish.

For production: Upgrade to Option C for reliability. Cost: $32 per unit, but eliminates 90% of hardware support issues.

Common Mistake: Insufficient Debouncing Causes Double-Login Attempts

Problem: Button mechanical bouncing can trigger multiple authentication attempts from a single press.

Impact:

User presses button once
ESP32 detects: Press -> Release -> Press -> Release (in 50ms)
System processes: 2 authentication attempts
Account locked out after 2 button presses (instead of 3)

Lab code correctly handles this:

const unsigned long DEBOUNCE_DELAY = 250;  // 250ms minimum between presses

if (digitalRead(BUTTON_ACCESS) == LOW &&
    (currentTime - lastButtonAccessTime) > DEBOUNCE_DELAY) {
    lastButtonAccessTime = currentTime;
    requestAccess(2);  // Only processes once
}

Testing: Press button rapidly 10 times. Should process exactly 10 requests (not 20-30).


17.8 Summary

In this setup chapter, you learned:

  1. Hardware components needed for an IoT access control system
  2. Circuit wiring for LEDs, buzzer, and buttons with ESP32
  3. Code organization with clear separation of concerns
  4. Access level hierarchy from GUEST to ADMIN
  5. Security configurations for lockout policies
  6. Constant-time comparison to prevent timing attacks
Lab Shortcuts vs Production

This lab intentionally shows what NOT to do in production:

Lab Shortcut Production Requirement
Hardcoded credentials Store in secure element (ATECC608B, TPM)
Plain text card IDs Encrypted credential storage
In-memory audit log Persistent, tamper-evident logging
Single-factor auth Multi-factor authentication (MFA)
Local database Centralized identity provider (LDAP, AD)

17.9 Knowledge Check

Common Pitfalls

The .env file containing JWT_SECRET, database credentials, and API keys must never be committed to git. Always add .env to .gitignore before the first commit, and use environment variable injection (CI/CD secrets, AWS Parameter Store) in deployment pipelines.

bcrypt with 4 rounds (the minimum) hashes in <1 ms — fast enough for an attacker to try millions of passwords per second. The recommended rounds for production is 12 (2^12 iterations), which takes ~250 ms per hash — acceptable for login but infeasible for brute force.

Accepting username and password fields directly from the request body without validation allows SQL injection (in raw queries), NoSQL injection, and excessively long strings that cause DoS. Always validate and sanitize all user inputs before using them in database operations.

SQLite is single-writer, file-based, and lacks the concurrency, replication, and backup capabilities needed for production IoT platforms. Use PostgreSQL or MySQL for production, and design your data access layer with an ORM or query builder to make migration straightforward.

17.10 What’s Next

Continue to the full implementation:

If you want to… Read this
Implement the access control code Lab: Access Control Implementation
Learn advanced access control concepts Advanced Access Control Concepts
See the full lab overview Authentication and Access Control Overview
Understand zero trust principles Zero Trust Security
Study authentication fundamentals Auth & Authorization Basics

Key Concepts

  • Express.js Middleware: Functions that execute in sequence for every HTTP request; authentication middleware validates tokens before requests reach route handlers
  • bcrypt.hash(): The function call that produces a salted hash of a password; the cost factor (rounds) determines computation time and security margin
  • JWT Sign/Verify: jwt.sign() creates a signed token; jwt.verify() validates the signature and extracts claims; the secret/key must be kept server-side
  • SQLite (Dev Database): A file-based SQL database suitable for development and testing; replaced with PostgreSQL or similar for production deployments
  • Environment Variables: Configuration values (JWT secret, database URL, port) stored outside the codebase; loaded with dotenv to avoid hardcoding secrets
  • Express Router: A mini-application that handles routing for a subset of paths; used to organize authentication routes separately from application routes
  • HTTP Status Codes: 200 (OK), 201 (Created), 401 (Unauthorized — not authenticated), 403 (Forbidden — authenticated but not authorized), 409 (Conflict — resource already exists)
In 60 Seconds

This lab sets up the foundational Node.js/Express environment with bcrypt password hashing, JWT token generation, and database initialization — the infrastructure layer that all subsequent authentication and authorization features are built upon.