7  Lab: Auth & Access Control

7.1 Learning Objectives

By completing this hands-on lab, you will be able to:

  • Implement token-based authentication with simulated RFID credentials for IoT access control
  • Design multi-level access control with distinct admin, user, and guest permission tiers
  • Apply account lockout policies to prevent brute force credential attacks
  • Build audit logging systems that track all authentication and access events
  • Create intuitive feedback mechanisms using LEDs and buzzers to communicate security states
  • Understand the relationship between authentication (who you are) and authorization (what you can do)

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.

“Today we build a real security system on an ESP32!” Max the Microcontroller said, setting up the lab bench. “We will use RFID cards for authentication and LEDs to show access levels. Green for admin, blue for user, yellow for guest, and red for denied!”

Sammy the Sensor held up an RFID card. “When someone taps this card on the reader, I detect its unique ID number. But just reading the card is not enough – we need to check if this ID is in our database of allowed users. That is authentication: proving you are who you claim to be.”

“Then comes authorization!” Lila the LED chimed in. “Once we know WHO you are, we check WHAT you are allowed to do. The admin card can control everything. The user card can read data and control basic functions. The guest card can only view information. I change my color to show which level was granted!”

“We also add account lockout,” Bella the Battery explained. “If someone tries three wrong cards in a row, the system locks out for 60 seconds. This prevents brute force attacks where someone rapidly tries different cards hoping to find one that works. And everything gets logged – which card was scanned, when, and what happened. Building this lab gives you hands-on experience with the exact security concepts used in real-world systems!”

Prerequisites

Before starting this lab, you should be familiar with:

  • Basic Arduino/C++ programming concepts
  • Understanding of authentication vs authorization concepts (see Cyber Security Methods)
  • Familiarity with ESP32 GPIO operations

7.2 Authentication vs Authorization: Understanding the Difference

Before building our access control system, let’s clarify two concepts that are often confused:

Flowchart showing the two-step access control process where a credential is first checked against a user database for authentication (answering who are you), and if recognized, the user's role is checked against zone permissions for authorization (answering what can you do), with separate denial paths for unknown credentials and insufficient privileges
Figure 7.1: The two-step access control process: authentication verifies identity, authorization determines permissions
Concept Question Answered Example Failure Mode
Authentication “Who are you?” RFID card, password, fingerprint “I don’t recognize you”
Authorization “What can you do?” Admin vs user privileges “You don’t have permission”

Key insight: Authentication must happen BEFORE authorization. You cannot determine what someone is allowed to do until you know who they are.


7.3 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

7.4 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.


7.5 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)
Wiring diagram showing an ESP32 DevKit with GPIO 2, 4, 5, and 18 connected through 220 ohm resistors to green, red, yellow, and blue LEDs respectively, GPIO 19 connected to a piezo buzzer, and GPIO 15 and 16 connected to push buttons with internal pullups, all sharing a common ground rail
Figure 7.2: Circuit diagram showing ESP32 connections to LEDs, buzzer, and buttons

7.6 Complete Access Control Implementation Code

Copy this code into the Wokwi editor:

/*
 * 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_CARD = 15;  // Button to cycle through RFID cards
const int BUTTON_REQUEST_ACCESS = 16; // Button to request access to a zone

// ============== SYSTEM PARAMETERS ==============
const int MAX_FAILED_ATTEMPTS = 3;
const unsigned long LOCKOUT_DURATION_MS = 60000; // 60 seconds
const unsigned long BUTTON_DEBOUNCE_MS = 200;

// ============== ENUMERATIONS ==============
enum AccessLevel {
  GUEST = 0,
  USER = 1,
  ADMIN = 2
};

enum AccountStatus {
  ACTIVE = 0,
  DISABLED = 1,
  LOCKED = 2
};

// ============== STRUCTURES ==============
struct User {
  const char* cardID;
  const char* name;
  AccessLevel level;
  AccountStatus status;
  int failedAttempts;
  unsigned long lockoutUntil;
};

struct Zone {
  const char* name;
  AccessLevel requiredLevel;
};

// ============== USER DATABASE ==============
User users[] = {
  {"CARD_001", "Alice Admin", ADMIN, ACTIVE, 0, 0},
  {"CARD_002", "Charlie User", USER, ACTIVE, 0, 0},
  {"CARD_003", "Eve Guest", GUEST, ACTIVE, 0, 0},
  {"CARD_004", "Frank Former", USER, DISABLED, 0, 0}, // Disabled account
};
const int NUM_USERS = sizeof(users) / sizeof(users[0]);

// ============== ZONE DATABASE ==============
Zone zones[] = {
  {"Public Lobby", GUEST},
  {"Office Area", USER},
  {"Server Room", ADMIN},
  {"Control Center", ADMIN}
};
const int NUM_ZONES = sizeof(zones) / sizeof(zones[0]);

// ============== GLOBAL STATE ==============
int currentCardIndex = 0;
int currentZoneIndex = 0;
unsigned long lastButtonPress = 0;

// ============== FUNCTION DECLARATIONS ==============
bool constantTimeStringCompare(const char* a, const char* b);
int authenticateCard(const char* cardID);
bool checkAuthorization(int userIndex, int zoneIndex);
bool checkAccountStatus(int userIndex);
void logAuditEvent(const char* event, const char* details);
void grantAccess(const char* userName, const char* zoneName, bool isAdmin);
void denyAccess(const char* reason);
void handleFailedAttempt(int userIndex);
void processAccessRequest();
void playSuccessTone();
void playErrorTone();
void displayUserDatabase();
void displayZoneDatabase();

// ============== SETUP ==============
void setup() {
  Serial.begin(115200);
  delay(500);

  // Configure pins
  pinMode(LED_ACCESS_GRANTED, OUTPUT);
  pinMode(LED_ACCESS_DENIED, OUTPUT);
  pinMode(LED_SYSTEM_STATUS, OUTPUT);
  pinMode(LED_ADMIN_MODE, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(BUTTON_SELECT_CARD, INPUT_PULLUP);
  pinMode(BUTTON_REQUEST_ACCESS, INPUT_PULLUP);

  // Initial state
  digitalWrite(LED_SYSTEM_STATUS, HIGH); // System ready
  delay(100);
  digitalWrite(LED_SYSTEM_STATUS, LOW);

  Serial.println("\n========================================");
  Serial.println("  SECURE IoT ACCESS CONTROL SYSTEM");
  Serial.println("========================================\n");
  Serial.println("Demonstrating Authentication & Authorization\n");

  displayUserDatabase();
  displayZoneDatabase();

  Serial.println("\n========================================");
  Serial.println("CONTROLS:");
  Serial.println("  Button 1: Select RFID Card");
  Serial.println("  Button 2: Request Access to Zone");
  Serial.println("========================================\n");
}

// ============== MAIN LOOP ==============
void loop() {
  unsigned long currentTime = millis();

  // Handle card selection button
  if (digitalRead(BUTTON_SELECT_CARD) == LOW &&
      (currentTime - lastButtonPress > BUTTON_DEBOUNCE_MS)) {
    lastButtonPress = currentTime;
    currentCardIndex = (currentCardIndex + 1) % NUM_USERS;

    Serial.print("\n[CARD SELECTED] ");
    Serial.print(users[currentCardIndex].cardID);
    Serial.print(" (");
    Serial.print(users[currentCardIndex].name);
    Serial.println(")");

    // Brief LED flash
    digitalWrite(LED_SYSTEM_STATUS, HIGH);
    delay(100);
    digitalWrite(LED_SYSTEM_STATUS, LOW);
  }

  // Handle access request button
  if (digitalRead(BUTTON_REQUEST_ACCESS) == LOW &&
      (currentTime - lastButtonPress > BUTTON_DEBOUNCE_MS)) {
    lastButtonPress = currentTime;

    // Cycle to next zone
    currentZoneIndex = (currentZoneIndex + 1) % NUM_ZONES;

    processAccessRequest();
  }

  delay(10);
}

// ============== ACCESS REQUEST HANDLER ==============
void processAccessRequest() {
  Serial.println("\n========================================");
  Serial.print("ACCESS REQUEST - Zone: ");
  Serial.println(zones[currentZoneIndex].name);
  Serial.println("========================================");

  // Step 1: AUTHENTICATION - Who are you?
  const char* cardID = users[currentCardIndex].cardID;
  Serial.print("\n[AUTHENTICATION] Card presented: ");
  Serial.println(cardID);

  int userIndex = authenticateCard(cardID);

  if (userIndex == -1) {
    // Authentication failed - unknown card
    Serial.println("[AUTHENTICATION] FAILED - Unknown card");
    logAuditEvent("AUTH_FAIL", cardID);
    denyAccess("Unknown credential");
    Serial.println("========================================\n");
    return;
  }

  // Authentication succeeded
  Serial.print("[AUTHENTICATION] SUCCESS - Identified as: ");
  Serial.println(users[userIndex].name);
  logAuditEvent("AUTH_SUCCESS", users[userIndex].name);

  // Check account status before proceeding
  if (!checkAccountStatus(userIndex)) {
    Serial.println("========================================\n");
    return;
  }

  // Step 2: AUTHORIZATION - What can you do?
  Serial.print("\n[AUTHORIZATION] Checking permissions for zone: ");
  Serial.println(zones[currentZoneIndex].name);
  Serial.print("User level: ");
  Serial.print(users[userIndex].level);
  Serial.print(" | Required level: ");
  Serial.println(zones[currentZoneIndex].requiredLevel);

  bool authorized = checkAuthorization(userIndex, currentZoneIndex);

  if (authorized) {
    Serial.println("[AUTHORIZATION] GRANTED");
    logAuditEvent("ACCESS_GRANTED", users[userIndex].name);
    grantAccess(users[userIndex].name,
                zones[currentZoneIndex].name,
                users[userIndex].level == ADMIN);

    // Reset failed attempts on successful access
    users[userIndex].failedAttempts = 0;
  } else {
    Serial.println("[AUTHORIZATION] DENIED - Insufficient privileges");
    logAuditEvent("ACCESS_DENIED", "Insufficient privileges");
    denyAccess("Insufficient privileges");
  }

  Serial.println("========================================\n");
}

// ============== SECURITY FUNCTIONS ==============

// Constant-time string comparison (prevents timing attacks)
// Note: This is a simplified educational implementation. Production
// systems should use a fixed-length token comparison or a library
// function like CRYPTO_memcmp() that guarantees constant-time behavior.
bool constantTimeStringCompare(const char* a, const char* b) {
  int lenA = strlen(a);
  int lenB = strlen(b);
  int maxLen = max(lenA, lenB);
  int diff = 0;

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

  diff |= (lenA ^ lenB);

  return (diff == 0);
}

// Authenticate card (returns user index or -1 if not found)
int authenticateCard(const char* cardID) {
  for (int i = 0; i < NUM_USERS; i++) {
    if (constantTimeStringCompare(users[i].cardID, cardID)) {
      return i;
    }
  }
  return -1; // Unknown card
}

// Check authorization (does user have required access level?)
bool checkAuthorization(int userIndex, int zoneIndex) {
  return users[userIndex].level >= zones[zoneIndex].requiredLevel;
}

// Check account status (active, disabled, locked)
bool checkAccountStatus(int userIndex) {
  unsigned long currentTime = millis();

  // Check if account is permanently disabled
  if (users[userIndex].status == DISABLED) {
    Serial.println("[STATUS] Account is DISABLED");
    logAuditEvent("ACCESS_DENIED", "Account disabled");
    denyAccess("Account disabled");
    return false;
  }

  // Check if account is temporarily locked
  if (users[userIndex].status == LOCKED) {
    if (currentTime < users[userIndex].lockoutUntil) {
      unsigned long remainingTime = (users[userIndex].lockoutUntil - currentTime) / 1000;
      Serial.print("[STATUS] Account LOCKED for ");
      Serial.print(remainingTime);
      Serial.println(" more seconds");
      denyAccess("Account locked - brute force protection");
      return false;
    } else {
      // Lockout expired - unlock account
      users[userIndex].status = ACTIVE;
      users[userIndex].failedAttempts = 0;
      Serial.println("[STATUS] Lockout expired - account unlocked");
    }
  }

  return true;
}

// Log audit event (timestamp + event + details)
void logAuditEvent(const char* event, const char* details) {
  Serial.print("[AUDIT] ");
  Serial.print(millis());
  Serial.print(" | ");
  Serial.print(event);
  Serial.print(" | ");
  Serial.println(details);
}

// Grant access with visual/audio feedback
void grantAccess(const char* userName, const char* zoneName, bool isAdmin) {
  Serial.print("\n>> ACCESS GRANTED to ");
  Serial.print(userName);
  Serial.print(" for ");
  Serial.println(zoneName);

  // Visual feedback
  digitalWrite(LED_ACCESS_GRANTED, HIGH);
  if (isAdmin) {
    digitalWrite(LED_ADMIN_MODE, HIGH); // Blue LED for admin
  }

  // Audio feedback
  playSuccessTone();

  delay(2000);
  digitalWrite(LED_ACCESS_GRANTED, LOW);
  digitalWrite(LED_ADMIN_MODE, LOW);
}

// Deny access with visual/audio feedback
void denyAccess(const char* reason) {
  Serial.print("\n>> ACCESS DENIED - ");
  Serial.println(reason);

  // Visual feedback
  for (int i = 0; i < 3; i++) {
    digitalWrite(LED_ACCESS_DENIED, HIGH);
    delay(200);
    digitalWrite(LED_ACCESS_DENIED, LOW);
    delay(200);
  }

  // Audio feedback
  playErrorTone();
}

// Handle failed authentication attempt
void handleFailedAttempt(int userIndex) {
  users[userIndex].failedAttempts++;

  Serial.print("[SECURITY] Failed attempt count: ");
  Serial.println(users[userIndex].failedAttempts);

  if (users[userIndex].failedAttempts >= MAX_FAILED_ATTEMPTS) {
    users[userIndex].status = LOCKED;
    users[userIndex].lockoutUntil = millis() + LOCKOUT_DURATION_MS;

    Serial.println("\n!! SECURITY ALERT: Account locked due to multiple failed attempts");
    Serial.println("Possible brute force attack detected!");

    // Yellow LED blinking for security alert
    for (int i = 0; i < 5; i++) {
      digitalWrite(LED_SYSTEM_STATUS, HIGH);
      tone(BUZZER_PIN, 1000, 100);
      delay(200);
      digitalWrite(LED_SYSTEM_STATUS, LOW);
      delay(200);
    }

    logAuditEvent("ACCOUNT_LOCKED", users[userIndex].name);
  }
}

// Audio feedback - success
void playSuccessTone() {
  tone(BUZZER_PIN, 2000, 100);
  delay(150);
  tone(BUZZER_PIN, 2500, 100);
}

// Audio feedback - error
void playErrorTone() {
  tone(BUZZER_PIN, 500, 200);
  delay(250);
  tone(BUZZER_PIN, 300, 200);
}

// Display user database at startup
void displayUserDatabase() {
  Serial.println("REGISTERED USERS:");
  Serial.println("================");
  for (int i = 0; i < NUM_USERS; i++) {
    Serial.print(i);
    Serial.print(". ");
    Serial.print(users[i].name);
    Serial.print(" (");
    Serial.print(users[i].cardID);
    Serial.print(") - Level: ");
    Serial.print(users[i].level);
    Serial.print(" | Status: ");
    Serial.println(users[i].status == ACTIVE ? "ACTIVE" : "DISABLED");
  }
  Serial.println();
}

// Display zone database at startup
void displayZoneDatabase() {
  Serial.println("ACCESS ZONES:");
  Serial.println("=============");
  for (int i = 0; i < NUM_ZONES; i++) {
    Serial.print(i);
    Serial.print(". ");
    Serial.print(zones[i].name);
    Serial.print(" - Requires: ");
    switch(zones[i].requiredLevel) {
      case GUEST: Serial.println("GUEST"); break;
      case USER: Serial.println("USER"); break;
      case ADMIN: Serial.println("ADMIN"); break;
    }
  }
  Serial.println();
}

7.7 Step-by-Step Instructions

7.7.1 Step 1: Set Up the Circuit

  1. Open the Wokwi simulator above
  2. Add components from the parts panel:
    • 1x ESP32 DevKit V1
    • 4x LEDs (green, red, yellow, blue)
    • 4x 220 ohm resistors
    • 1x Piezo buzzer
    • 2x Push buttons
  3. Wire connections as shown in the circuit diagram
  4. Connect LED anodes (long leg) to ESP32 pins through resistors
  5. Connect LED cathodes (short leg) and buzzer negative to GND
  6. Connect buttons between GPIO pins and GND (internal pullup used)

7.7.2 Step 2: Enter the Code

  1. Copy the complete code above
  2. Paste into the Wokwi code editor (left panel)
  3. Ensure the code compiles without errors
  4. Click “Start Simulation”

7.7.3 Step 3: Test Authentication Scenarios

Open the Serial Monitor and try these scenarios:

Scenario A: Valid Admin Access

Press Button 1 to select Card 0 (Alice Admin), then press Button 2 to request access.

Expected: Alice Admin (ADMIN level) accessing a zone - Green LED on + Blue LED on (admin indicator) - High-pitched success tones - “ACCESS GRANTED” message

Scenario B: Valid User - Insufficient Privileges

Press Button 1 to cycle to Card 1 (Charlie User), then press Button 2 until you reach the Server Room zone.

Expected: Charlie User (USER level) trying Server Room (needs ADMIN) - Red LED flashes - Low error tones - “ACCESS DENIED - Insufficient Privileges” message - Authentication PASSED but Authorization FAILED

Scenario C: Guest Access to Public Area

Press Button 1 to cycle to Card 2 (Eve Guest), then press Button 2 until you reach the Public Lobby zone.

Expected: Eve Guest accessing Public Lobby - Green LED on (no blue - not admin) - Success tones - “ACCESS GRANTED” message

Scenario D: Disabled Account

Press Button 1 to cycle to Card 3 (Frank Former), then press Button 2 to request access.

Expected: Frank Former (disabled account) trying any zone - Red LED flashes - “ACCESS DENIED - Account disabled” message - Authentication identifies the user, but the account is not active

Understanding Brute Force Prevention

In this lab, the button-based interface cycles through known cards in the database, so the lockout scenario is demonstrated when a recognized but locked account attempts access. In a real RFID system, unknown cards would be rejected at the authentication stage, and repeated failed scans from unknown cards would trigger system-wide rate limiting rather than per-account lockout.

To observe the lockout mechanism, you can modify the code to track failed authorization attempts (e.g., a USER-level card repeatedly requesting an ADMIN-level zone) and lock the account after 3 denied attempts.


7.8 Knowledge Check

A manufacturing company deploys ESP32-based access control at 24 entry points across 3 buildings. The system uses 125 kHz RFID badges with a 3-tier role hierarchy.

User Database:

User users[] = {
    {"CARD_A01", "Alice Manager",   ADMIN,  ACTIVE, 0, 0},  // Full access
    {"CARD_E15", "Bob Engineer",    USER,   ACTIVE, 0, 0},  // Labs + office
    {"CARD_V08", "Carol Visitor",   GUEST,  ACTIVE, 0, 0},  // Lobby only
    {"CARD_E22", "Dave Former",     USER, DISABLED, 0, 0},  // Terminated
};

Zone Access Matrix:

            Lobby   Office   Lab   Server Room
GUEST         Y       X       X        X
USER          Y       Y       Y        X
ADMIN         Y       Y       Y        Y

Sample Access Attempt Scenario:

  1. Bob (USER) scans badge at Lab door at 10:30 AM
  2. ESP32 reads card UID: 0x04E215BA (matches CARD_E15)
  3. Authentication: Card UID maps to “Bob Engineer” –> SUCCESS
  4. Account status check: ACTIVE –> PASS
  5. Authorization: USER level >= USER required for Lab –> GRANTED
  6. Audit log: [1698753000] Bob Engineer | ACCESS_GRANTED | Lab | USER

Failed Attempt:

  1. Carol (GUEST) tries Server Room at 2:15 PM
  2. Authentication: CARD_V08 –> “Carol Visitor” –> SUCCESS
  3. Authorization: GUEST level < ADMIN required –> DENIED
  4. Audit log: [1698766500] Carol Visitor | ACCESS_DENIED | Server Room | Insufficient privileges

Production Scale: Deployed system handles 850 badge scans/day with 99.4% uptime. Average authentication+authorization time: 120 ms.

Model Complexity Use Case Example
Attribute-Based (ABAC) High Time/location/context rules “Allow if USER + business hours + assigned building”
Role-Based (RBAC) Medium Hierarchical orgs “Admin > User > Guest”
Access Control Lists (ACL) Low Per-resource permissions “Resource X: Allow user1, user2; Deny user3”
Capability-Based Medium Fine-grained permissions Bit flags: READ=0x01, WRITE=0x02, EXECUTE=0x04

Selection Criteria:

  • <10 roles, static permissions –> RBAC (simplest)
  • Need time/location/device-state rules –> ABAC
  • Per-user customization without role explosion –> Capability-based
  • Small user count, resource-focused –> ACL

RBAC Best For:

  • Badge access systems (physical security)
  • Manufacturing floor zones
  • Simple IoT deployments (<100 devices, <20 users)

ABAC Best For:

  • Smart buildings (access depends on time, floor, emergency status)
  • Healthcare (access depends on patient assignment, shift, location)
  • Complex compliance requirements (audit trails with context)
Common Mistake: Using Fixed-Length Delays for Rate Limiting

What practitioners do wrong: After failed authentication, they add a fixed delay (e.g., delay(2000) for 2 seconds) thinking this slows brute force attacks.

Why it fails:

  1. Blocks legitimate users equally: A user who mistyped their password waits 2 seconds just like an attacker
  2. Does not scale with severity: The 100th failed attempt gets the same 2-second delay as the 1st
  3. Resource exhaustion: An attacker can open many parallel connections, each causing a 2-second delay, consuming ESP32 resources

Correct approach - Account Lockout:

const int MAX_FAILED_ATTEMPTS = 3;
const unsigned long LOCKOUT_DURATION = 60000;  // 60 seconds

struct User {
    const char* cardID;
    const char* name;
    int failedAttempts;
    unsigned long lockoutUntil;
};

bool checkAccountStatus(User* user) {
    unsigned long now = millis();

    // Check if still locked out
    if (now < user->lockoutUntil) {
        Serial.println("Account locked - try again later");
        return false;
    }

    // Lockout expired - reset
    if (user->lockoutUntil > 0) {
        user->failedAttempts = 0;
        user->lockoutUntil = 0;
    }

    return true;
}

void handleFailedAuth(User* user) {
    user->failedAttempts++;

    if (user->failedAttempts >= MAX_FAILED_ATTEMPTS) {
        user->lockoutUntil = millis() + LOCKOUT_DURATION;
        Serial.println("Account locked for 60 seconds");
    }
}

Real-world consequence: A 2019 smart lock implementation used fixed 5-second delays. Attackers parallelized 100 connections, testing 1,200 PINs/minute (100 connections x 12 attempts/min). Account lockout with escalating timeouts reduced this to 3 attempts/device/minute, making brute force impractical.

7.9 Summary

By completing this lab, you have:

  1. Implemented token-based authentication using simulated RFID cards
  2. Built a role-based access control system with three distinct permission levels
  3. Applied account lockout policies with escalating timeouts to prevent brute force attacks
  4. Created comprehensive audit logging for security forensics
  5. Designed intuitive feedback systems using LEDs and audio
  6. Understood the critical difference between authentication (identity verification) and authorization (permission checking)

These concepts directly translate to production IoT security systems, enterprise access control, and cloud IAM policies.

Password entropy \(H\) measures the unpredictability of a password in bits, calculated from password length \(L\) and character set size \(N\).

\[H = L \times \log_2(N)\]

The time \(T\) to brute force a password depends on entropy and the attacker’s rate \(r\) (attempts/second):

\[T = \frac{2^H}{r}\]

Working through an example:

Given: IoT device authentication with different credential types: - 4-digit PIN: \(L = 4\), \(N = 10\) (digits 0-9) - 6-character alphanumeric: \(L = 6\), \(N = 62\) (a-z, A-Z, 0-9) - 8-character mixed: \(L = 8\), \(N = 94\) (all printable ASCII) - Attacker rate: \(r = 1{,}000\) attempts/second (network limited)

Step 1: Calculate entropy for each credential type

4-digit PIN: \[H_{\text{PIN}} = 4 \times \log_2(10) = 4 \times 3.32 = 13.3 \text{ bits}\]

6-character alphanumeric: \[H_{\text{6char}} = 6 \times \log_2(62) = 6 \times 5.95 = 35.7 \text{ bits}\]

8-character mixed: \[H_{\text{8char}} = 8 \times \log_2(94) = 8 \times 6.55 = 52.4 \text{ bits}\]

Step 2: Calculate brute force time for each type

4-digit PIN: \[T_{\text{PIN}} = \frac{2^{13.3}}{1{,}000} = \frac{10{,}000}{1{,}000} = 10 \text{ seconds}\]

6-character alphanumeric: \[T_{\text{6char}} = \frac{2^{35.7}}{1{,}000} = \frac{56.8 \text{ billion}}{1{,}000} \approx 658 \text{ days}\]

8-character mixed: \[T_{\text{8char}} = \frac{2^{52.4}}{1{,}000} = \frac{5.9 \text{ quadrillion}}{1{,}000} \approx 187{,}000 \text{ years}\]

Result: A 4-digit PIN can be cracked in 10 seconds, while an 8-character mixed password requires 187,000 years at 1,000 attempts/second. Each additional character exponentially increases security.

In practice: Constrained IoT devices often use short PINs (4-6 digits) for usability, creating weak entropy (13-20 bits). Account lockout policies compensate by limiting the attacker’s rate: with 3-attempt lockout and 60-second penalty, effective rate drops to \(r \approx 0.05\) attempts/second, increasing brute force time from 10 seconds to about 2.3 days for a 4-digit PIN. For production IoT, prefer certificate-based authentication (128-256 bits of entropy) over PINs.

7.10 Concept Relationships

How Fundamentals Lab Concepts Connect
Core Concept Builds On Enables Common Confusion
Token-based authentication Credential verification Identity proof without passwords “Is a token the same as a password?” - No. Tokens are issued after authentication; passwords are used FOR authentication
Role-based access control Authentication success Permission enforcement via roles “Can users have multiple roles?” - In this lab, one role per user; advanced systems support role combinations
Account lockout Failed attempt tracking Brute force attack prevention “Why 60 seconds, not permanent?” - Balance security with usability; escalating lockouts increase with repeated failures
Audit logging All authentication/authorization events Security forensics and compliance “Why log successful access?” - Audit trails need both successes and failures to detect anomalies
Visual feedback System state Intuitive security status communication “Why LEDs if we have Serial?” - Physical indicators work without computer connection

Key Insight: This lab demonstrates the authentication –> authorization flow, where proving identity (who you are) must precede permission checking (what you can do). Both steps are essential for secure access control.

Common Pitfalls

jwt.verify() throws different errors for invalid signatures vs expired tokens. Treating all JWT errors as “unauthorized” obscures whether the issue is a bad token or simply an expired one that needs refresh. Catch JsonWebTokenError and TokenExpiredError separately and return appropriate responses.

Fetching data from the database and then checking if the user has permission to see it wastes database resources and risks accidentally returning data if the check is forgotten. Always check authorization before querying — deny first, then query only if authorized.

bcrypt.hashSync() blocks the Node.js event loop for ~100–400 ms during hashing. In a server handling many concurrent requests, this causes all other requests to wait. Always use the async functions (bcrypt.hash(), bcrypt.compare()) to avoid blocking.

The most common missed test case is what happens when an access token expires during a user session. Verify that expired tokens return 401, that the client correctly identifies expiry vs other errors, and that the refresh token flow successfully obtains a new access token.

7.11 What’s Next

If you want to… Read this
Try challenge exercises Challenge Exercises
Learn access control concepts in depth Auth & Authorization Concepts
Explore advanced access control Advanced Access Control Lab
Return to the overview Authentication and Access Control Overview
Study zero trust security Zero Trust Security

Now that you understand basic authentication and access control implementation, continue to:

Or return to the Authentication and Access Control Overview.

7.12 See Also

Related Security Topics:

Real-World Applications:

  • Corporate badge access systems (HID iCLASS, SALTO)
  • Smart locks (August, Yale, Schlage)
  • AWS IAM policies for cloud resource access
  • Kubernetes RBAC for container orchestration

Key Concepts

  • bcrypt Cost Factor: The number of rounds (2^cost iterations) used to hash a password; cost=10 takes ~100 ms, cost=12 takes ~400 ms — higher cost makes brute force exponentially harder
  • JWT Payload Claims: Standard fields in JWT including iss (issuer), sub (subject/user ID), exp (expiry), iat (issued at), and custom claims like role; all claims are base64-encoded (not encrypted)
  • Express Route Protection: Applying authenticateToken middleware to a route ensures only requests with valid JWTs reach the handler; unauthenticated requests receive 401 Unauthorized
  • RBAC Middleware: A function that reads the user’s role from the authenticated token and compares it to the required role for the endpoint; returns 403 Forbidden if the role is insufficient
  • Password Reset Flow: A secure flow where a short-lived reset token is emailed, validated on submission, and immediately invalidated after use — never allowing password changes without token verification
  • Token Expiry Strategy: Access tokens should be short-lived (15 min–1 hour) for security; refresh tokens should be longer-lived (7–30 days) but stored securely and rotated on each use
  • Async Error Handling: Express does not catch errors thrown in async route handlers by default; use try/catch in every async handler or a global error middleware wrapper
In 60 Seconds

This fundamental lab builds a working authentication and access control system from first principles — hashing passwords with bcrypt, generating JWTs, and enforcing RBAC in middleware — giving hands-on experience with the exact patterns used in production IoT platforms.


Key Takeaway

Authentication answers “Who are you?” while Authorization answers “What can you do?”

Always implement both. A system that only authenticates (verifies identity) but does not authorize (check permissions) gives all authenticated users full access. A system that tries to authorize without authenticating cannot know whose permissions to check.

The audit log ties it all together for the third “A” - Accounting - so you can investigate incidents after the fact.