14  Lab: Access Control Implementation

Complete Code with Testing Scenarios

authentication
auth
lab
basic

14.1 Start With the Story

Now picture the same controller during a test run. Alice’s admin card should open the server room, Eve’s guest card should not, a disabled card should fail, and repeated unknown cards should trigger lockout without leaking useful clues. This implementation chapter turns the setup into the decision path that answers three separate questions: who is this, what may they do, and what evidence did the system record?

The IoT punchline is that a secure access decision is a sequence, not one if statement. Start simple: run a known-good card, a known-bad card, and a lockout case, then check that the LEDs, buzzer, serial output, and audit log all tell the same story.

Chapter Roadmap

This chapter turns the setup lab into an end-to-end access decision:

  1. First load the credential database, access levels, zone table, security state, and audit-log structures.
  2. Then trace the code path from button input through lockout, authentication, authorization, feedback, and logging.
  3. Next run the five testing scenarios and connect each visible signal to the internal reason code.
  4. Finally compare the lab with production authentication choices, throttling, timing-safe comparison, and review quizzes.

Checkpoints recap key decisions. Deep-dive sections are useful for review, but you can skim them on a first pass.

Shield Shelly, the security guide

Shield Shelly

“Security is not a wall, it’s a list of who can do what — and the proof that the list is enforced.”

In this lab she traces the decision path — lockout first, then identity, then zone level — with the audit log as her evidence.

Lab execution time can be estimated before starting runs:

\[ T_{\text{total}} = N_{\text{runs}} \times (t_{\text{setup}} + t_{\text{run}} + t_{\text{review}}) \]

Worked example: With 5 runs and per-run times of 4 min setup, 6 min execution, and 3 min review, total lab time is \(5\times(4+6+3)=65\) minutes. This prevents under-scoping and helps schedule complete experimental cycles.

14.2 Learning Objectives

By completing this implementation, you will be able to:

  • Implement the full authentication flow from card scan to access decision
  • Build authorization logic with role-based access control
  • Create comprehensive audit logging for security forensics
  • Test multiple security scenarios including lockouts and disabled accounts
  • Understand the relationship between real-world systems and this lab implementation

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 implementation, you should:

  • Complete Lab: Basic Access Control Setup for circuit wiring and code structure
  • Use the setup from the previous lab; optional Wokwi or Arduino testing can come after you understand the code flow

Concept Relationships
Concept Related To Relationship Type
Token-Based Auth RFID, API Keys Simulates - Card IDs represent physical tokens in real access systems
RBAC (Role-Based) Access Levels, Permissions Implements - Admin/User/Guest hierarchy follows RBAC principles
Account Lockout Brute Force Defense Prevents - Escalating timeouts block automated password attacks
Constant-Time Compare Timing Attacks, Side-Channel Defends Against - XOR-based comparison prevents information leakage via timing
Audit Logging Security Forensics, Compliance Enables - Complete event trail supports incident investigation
Separation of Concerns AuthN vs AuthZ Demonstrates - Authentication (who?) separate from authorization (what?)
See Also

Foundation Concepts:

Related Security Topics:

Practical Applications:


14.3 Complete Implementation Code

Copy this complete code into the Arduino IDE or an optional Wokwi ESP32 sketch. This includes all functions from the setup plus the core authentication, authorization, and feedback logic. Paste all sections below in order into a single file.

14.3.1 Includes, Pin Definitions, and Data Structures

/*
 * Secure IoT Access Control System
 * Demonstrates: RFID-style authentication, role-based access control,
 *               lockout policies, and comprehensive audit logging
 */
#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;
const int BUTTON_SELECT = 15;
const int BUTTON_ACCESS = 16;

// Access levels and credential structure
enum AccessLevel { ACCESS_NONE=0, ACCESS_GUEST=1, ACCESS_USER=2, ACCESS_ADMIN=3 };

struct UserCredential {
    const char* cardId;
    const char* userName;
    AccessLevel accessLevel;
    bool isActive;
};

14.3.2 Credential Database and Security Configuration

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}
};

const int NUM_TEST_CARDS = 8;
const char* testCards[NUM_TEST_CARDS] = {
    "RFID_ADMIN_001", "RFID_USER_001", "RFID_GUEST_001", "RFID_DISABLED",
    "RFID_UNKNOWN_1", "RFID_UNKNOWN_2", "RFID_ADMIN_002", "RFID_USER_002"
};

const int MAX_FAILED_ATTEMPTS = 3;
const unsigned long LOCKOUT_DURATION_MS = 60000;
const unsigned long LOCKOUT_ESCALATION_MS = 30000;
const int MAX_LOCKOUT_DURATION_MS = 300000;

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}
};

14.3.3 Security State, Audit Log, and Forward Declarations

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

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;

unsigned long lastButtonSelectTime = 0;
unsigned long lastButtonAccessTime = 0;
const unsigned long DEBOUNCE_DELAY = 250;

// Forward declarations
void initializeSystem();  void handleButtons();
UserCredential* authenticateCard(const char* cardId);
bool authorizeAccess(AccessLevel userLevel, AccessLevel requiredLevel);
void requestAccess(int zoneIndex);
void logAuditEvent(const char* cardId, const char* userName,
    const char* eventType, const char* zoneName,
    bool success, AccessLevel level);
void printAuditLog();      void handleLockout();
void indicateAccessGranted(AccessLevel level);
void indicateAccessDenied(); void indicateSystemLocked();
void playTone(int frequency, int duration);
bool constantTimeCompare(const char* a, const char* b);
void processSerialCommands(); void printMenu();
const char* accessLevelToString(AccessLevel level);

Shield ShellyCheckpoint: Credential Model

You now know what the sketch must define before the loop starts:

  • The database contains 6 users and the simulator cycles through 8 test cards, so valid, disabled, and unknown cards all have test coverage.
  • Four zones use the same hierarchy as the four access levels: ACCESS_NONE, ACCESS_GUEST, ACCESS_USER, and ACCESS_ADMIN.
  • Lockout starts after 3 failed attempts, begins at 60000 ms, adds 30000 ms per escalation, and caps at 300000 ms.

14.3.4 Setup, Main Loop, and Initialization

void setup() {
    Serial.begin(115200);
    delay(1000);
    initializeSystem();
    Serial.println("\n=========================================");
    Serial.println("   IoT ACCESS CONTROL SYSTEM v1.0");
    Serial.println("=========================================");
    printMenu();
}

void loop() {
    handleButtons();
    handleLockout();
    processSerialCommands();
    delay(10);
}

void initializeSystem() {
    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, INPUT_PULLUP);
    pinMode(BUTTON_ACCESS, INPUT_PULLUP);
    // All LEDs off, reset security state
    for (int pin : {LED_ACCESS_GRANTED, LED_ACCESS_DENIED,
                    LED_SYSTEM_STATUS, LED_ADMIN_MODE})
        digitalWrite(pin, LOW);
    secState = {0, 0, false, 0, "", ACCESS_NONE, 0};
    playTone(1000, 100); delay(100);
    playTone(1500, 100); delay(100);
    playTone(2000, 150);
}

14.3.5 Authentication and Authorization

UserCredential* authenticateCard(const char* cardId) {
    for (int i = 0; i < NUM_USERS; i++)
        if (constantTimeCompare(cardId, userDatabase[i].cardId))
            return &userDatabase[i];
    return NULL;
}

bool constantTimeCompare(const char* a, const char* b) {
    size_t lenA = strlen(a), lenB = strlen(b);
    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;
    }
    result |= (lenA != lenB);
    return (result == 0);
}

bool authorizeAccess(AccessLevel userLevel, AccessLevel required) {
    return userLevel >= required;
}

14.3.6 Access Request Processing

This is the core logic that combines authentication (identity check), authorization (permission check), and lockout enforcement into a single access decision.

void requestAccess(int zoneIndex) {
    if (zoneIndex < 0 || zoneIndex >= NUM_ZONES) return;
    const char* cardId = testCards[secState.currentCardIndex];
    AccessZone* zone = &accessZones[zoneIndex];

    Serial.println("\n=== ACCESS REQUEST ===");
    Serial.printf("Card: %s -> Zone: %s (Requires: %s)\n",
        cardId, zone->zoneName,
        accessLevelToString(zone->requiredLevel));

    // Step 1: Check lockout
    if (secState.isLocked) {
        unsigned long remaining =
            (secState.lockoutEndTime - millis()) / 1000;
        Serial.printf("BLOCKED: Locked for %lu more seconds\n",
            remaining);
        logAuditEvent(cardId, "LOCKED_OUT", "ACCESS_BLOCKED",
            zone->zoneName, false, ACCESS_NONE);
        indicateSystemLocked();
        return;
    }

    // Step 2: Authentication -- who are you?
    UserCredential* user = authenticateCard(cardId);
    if (user == NULL) {
        secState.failedAttempts++;
        Serial.printf("AUTH FAILED: Unknown card (%d/%d)\n",
            secState.failedAttempts, MAX_FAILED_ATTEMPTS);
        if (secState.failedAttempts >= MAX_FAILED_ATTEMPTS) {
            unsigned long lockTime = LOCKOUT_DURATION_MS +
                ((secState.failedAttempts - MAX_FAILED_ATTEMPTS)
                 * LOCKOUT_ESCALATION_MS);
            if (lockTime > MAX_LOCKOUT_DURATION_MS)
                lockTime = MAX_LOCKOUT_DURATION_MS;
            secState.isLocked = true;
            secState.lockoutEndTime = millis() + lockTime;
            Serial.println("!!! BRUTE FORCE LOCKOUT !!!");
        }
        logAuditEvent(cardId, "UNKNOWN", "AUTH_FAILED",
            zone->zoneName, false, ACCESS_NONE);
        indicateAccessDenied();
        return;
    }
    if (!user->isActive) {
        logAuditEvent(cardId, user->userName, "ACCOUNT_DISABLED",
            zone->zoneName, false, user->accessLevel);
        indicateAccessDenied();
        return;
    }

    // Step 3: Authorization -- what can you do?
    if (authorizeAccess(user->accessLevel, zone->requiredLevel)) {
        Serial.printf("ACCESS GRANTED: %s -> %s\n",
            user->userName, zone->zoneName);
        secState.failedAttempts = 0;
        secState.lastAuthenticatedUser = user->userName;
        secState.currentAccessLevel = user->accessLevel;
        logAuditEvent(cardId, user->userName, "ACCESS_GRANTED",
            zone->zoneName, true, user->accessLevel);
        indicateAccessGranted(user->accessLevel);
    } else {
        Serial.printf("ACCESS DENIED: %s has %s, needs %s\n",
            user->userName,
            accessLevelToString(user->accessLevel),
            accessLevelToString(zone->requiredLevel));
        logAuditEvent(cardId, user->userName, "ACCESS_DENIED_AUTHZ",
            zone->zoneName, false, user->accessLevel);
        indicateAccessDenied();
    }
}

Shelly’s Access Ledger

  • Who: step two resolves the card to a named user before any zone question is asked.
  • What: step three grants entry only when the user’s level meets the zone’s required level.
  • Proof: every branch — locked, unknown, disabled, denied, granted — writes its own audit entry.

Shield ShellyCheckpoint: Decision Path

You now know how one request becomes a security decision:

  • The handler checks lockout before identity, so a locked system records ACCESS_BLOCKED without revealing whether the presented card is known.
  • Unknown cards increment failedAttempts; known inactive cards fail separately as ACCOUNT_DISABLED.
  • A successful authorization resets failures, stores the user and access level, writes ACCESS_GRANTED, and then drives the grant feedback.

With the grant/deny branch mapped, the next question is how the device reports the decision.

14.3.7 Lockout, Audit Logging, and Feedback

void handleLockout() {
    if (secState.isLocked && millis() >= secState.lockoutEndTime) {
        secState.isLocked = false;
        secState.failedAttempts = 0;
        Serial.println("Lockout expired -- system unlocked.");
        playTone(1000, 200); delay(100); playTone(1500, 200);
    }
}

void logAuditEvent(const char* cardId, const char* userName,
    const char* eventType, const char* zoneName,
    bool success, AccessLevel level) {
    AuditEntry* e = &auditLog[auditIndex];
    e->timestamp = millis(); e->cardId = cardId;
    e->userName = userName;  e->eventType = eventType;
    e->zoneName = zoneName;  e->success = success;
    e->attemptedLevel = level;
    auditIndex = (auditIndex + 1) % MAX_AUDIT_ENTRIES;
}

void indicateAccessGranted(AccessLevel level) {
    digitalWrite(LED_ACCESS_GRANTED, HIGH);
    digitalWrite(LED_ACCESS_DENIED, LOW);
    if (level == ACCESS_ADMIN) {
        digitalWrite(LED_ADMIN_MODE, HIGH);
        playTone(2000, 100); delay(50);
        playTone(2500, 100); delay(50);
        playTone(3000, 200);
    } else {
        playTone(1500, 150); delay(50);
        playTone(2000, 200);
    }
    delay(2000);
    digitalWrite(LED_ACCESS_GRANTED, LOW);
    digitalWrite(LED_ADMIN_MODE, LOW);
}

void indicateAccessDenied() {
    for (int i = 0; i < 3; i++) {
        digitalWrite(LED_ACCESS_DENIED, HIGH);
        playTone(400, 100); delay(100);
        digitalWrite(LED_ACCESS_DENIED, LOW); delay(100);
    }
}

void indicateSystemLocked() {
    for (int i = 0; i < 5; i++) {
        digitalWrite(LED_SYSTEM_STATUS, HIGH);
        digitalWrite(LED_ACCESS_DENIED, HIGH);
        playTone(300, 100); delay(100);
        digitalWrite(LED_SYSTEM_STATUS, LOW);
        digitalWrite(LED_ACCESS_DENIED, LOW); delay(100);
    }
}

void playTone(int frequency, int duration) {
    tone(BUZZER_PIN, frequency, duration);
}

14.3.8 Button Handling and Serial Commands

void handleButtons() {
    unsigned long t = millis();
    if (digitalRead(BUTTON_SELECT) == LOW &&
        (t - lastButtonSelectTime) > DEBOUNCE_DELAY) {
        lastButtonSelectTime = t;
        secState.currentCardIndex =
            (secState.currentCardIndex + 1) % NUM_TEST_CARDS;
        Serial.printf("Card selected: %s\n",
            testCards[secState.currentCardIndex]);
    }
    if (digitalRead(BUTTON_ACCESS) == LOW &&
        (t - lastButtonAccessTime) > DEBOUNCE_DELAY) {
        lastButtonAccessTime = t;
        requestAccess(2);  // Default: Server Room
    }
}

void processSerialCommands() {
    if (!Serial.available()) return;
    String cmd = Serial.readStringUntil('\n');
    cmd.trim(); cmd.toUpperCase();

    if (cmd == "HELP") printMenu();
    else if (cmd == "LOG")  printAuditLog();
    else if (cmd == "STATUS") {
        Serial.printf("Locked: %s | Fails: %d | Card: %s\n",
            secState.isLocked ? "YES":"NO",
            secState.failedAttempts,
            testCards[secState.currentCardIndex]);
    }
    else if (cmd.startsWith("ZONE "))
        requestAccess(cmd.substring(5).toInt());
    else if (cmd.startsWith("CARD ")) {
        int n = cmd.substring(5).toInt();
        if (n >= 0 && n < NUM_TEST_CARDS)
            secState.currentCardIndex = n;
    }
    else if (cmd == "RESET") {
        secState.failedAttempts = 0;
        secState.isLocked = false;
    }
}

void printMenu() {
    Serial.println("Commands: HELP STATUS USERS ZONES");
    Serial.println("  ZONE n | CARD n | LOG | RESET");
}

const char* accessLevelToString(AccessLevel level) {
    switch (level) {
        case ACCESS_NONE: return "NONE";
        case ACCESS_GUEST: return "GUEST";
        case ACCESS_USER: return "USER";
        case ACCESS_ADMIN: return "ADMIN";
        default: return "UNKNOWN";
    }
}

void printAuditLog() {
    Serial.println("\n=== SECURITY AUDIT LOG ===");
    for (int i = 0; i < MAX_AUDIT_ENTRIES; i++) {
        AuditEntry* e = &auditLog[i];
        if (e->timestamp > 0)
            Serial.printf("%8lu | %-16s | %-12s | %-13s | %s\n",
                e->timestamp, e->eventType, e->userName,
                e->zoneName, e->success ? "OK":"FAIL");
    }
}

Shield ShellyCheckpoint: Signals and Audit

You now know how the lab makes the code path observable:

  • The audit ring keeps up to 50 entries with timestamp, card ID, user name, event type, zone name, outcome, and attempted level.
  • The access button calls requestAccess(2), so the default hardware path tests the Server Room unless a serial ZONE n command changes the target.
  • A 250 ms debounce delay keeps one button press from turning into repeated access requests.

The code is complete enough to run; now the scenarios separate the main outcomes.


14.4 Testing Scenarios

14.4.1 Scenario A: Valid Admin Access

CARD 0
ZONE 2

Expected Results:

  • Alice Admin (ADMIN level) accessing Server Room
  • Green LED on + Blue LED on (admin indicator)
  • High-pitched success tones
  • “ACCESS GRANTED” message

14.4.2 Scenario B: Valid User - Insufficient Privileges

CARD 1
ZONE 2

Expected Results:

  • 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

14.4.3 Scenario C: Guest Access to Public Area

CARD 2
ZONE 0

Expected Results:

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

14.4.4 Scenario D: Unknown Card (Brute Force Prevention)

CARD 4
ZONE 0
ZONE 0
ZONE 0

Expected Results after 3 failed attempts:

  • Yellow LED blinking
  • System LOCKED for 60 seconds
  • “Possible brute force attack detected” message

14.4.5 Scenario E: Disabled Account

CARD 3
ZONE 0

Expected Results:

  • Frank Former (disabled account)
  • Red LED flashes
  • “Account DISABLED” message

Shelly’s Access Ledger

  • Who: Charlie’s card authenticates successfully; the system knows exactly who he is.
  • What: the Server Room requires ADMIN, so his USER level fails authorization, not authentication.
  • Proof: after three unknown-card attempts the system locks for 60 seconds and reports the brute-force detection.

14.5 Security Concepts Demonstrated

Concept Implementation Real-World Application
Token-Based Auth RFID card IDs matched against database Physical access cards, API keys, JWT tokens
Role-Based Access Control GUEST < USER < ADMIN hierarchy Corporate access levels, AWS IAM roles
Account Lockout 60s lockout after 3 failures, escalating Windows account lockout, banking apps
Disabled Accounts Active flag checked during authentication Employee offboarding, compromised accounts
Constant-Time Compare XOR comparison prevents timing attacks Secure password validation, crypto libraries
Audit Logging Complete event trail with timestamps SIEM systems, compliance logging
Separation of Concerns Authentication separate from Authorization OAuth 2.0 (AuthN) vs resource permissions (AuthZ)
Defense in Depth Multiple checks: valid card, active account, sufficient level Enterprise security architecture

14.6 Challenge Exercises

Challenge 1: Add Time-Based Access Control

Modify the system so certain zones are only accessible during “business hours” (simulate with a time window): - Server Room: Only accessible 9 AM - 5 PM - After hours, even admins need a special override code - Log all after-hours access attempts

Challenge 2: Implement Two-Person Rule

For high-security zones (Control Center), require two different admin cards within 30 seconds: 1. First admin presents card 2. System prompts “Waiting for second administrator…” 3. Second admin presents different card within time limit 4. Both must be admin level 5. If timeout or same card, deny access

Challenge 3: Add Temporary Access Passes

Implement visitor passes with expiration: - Create a new ACCESS_VISITOR level - Visitor cards expire after a set time (e.g., 8 hours from first use) - Track first-use timestamp per card - Automatically disable expired passes

Challenge 4: Implement Anti-Passback

Prevent “tailgating” by tracking entry/exit: - Users must exit a zone before re-entering - If card used for entry without previous exit, deny access - Track current zone for each user - Alert on anti-passback violations

The challenges extend the same implementation pattern. Before adding them, compare each lab shortcut with the production control it stands in for.


14.7 Connecting to Real-World Systems

Lab Implementation Production Equivalent
RFID card IDs Smart cards with cryptographic challenge-response
Hardcoded user database LDAP/Active Directory integration
Serial monitor Secure management API (TLS + client certificates)
LED indicators Security Operations Center (SOC) dashboards
Buzzer alerts SIEM integration, SMS/email notifications
Simple lockout Adaptive authentication with risk scoring
Role hierarchy Attribute-Based Access Control (ABAC)

Real-world systems these concepts apply to:

  • Corporate badge access (HID, SALTO)
  • Data center entry (biometric + card + PIN)
  • AWS IAM policies (principals, resources, actions)
  • Kubernetes RBAC (roles, bindings, service accounts)
  • OAuth 2.0 / OpenID Connect (tokens, scopes, claims)

Scenario: Your smart building access control system has 3,000 employees. You need to choose appropriate lockout settings that balance security with user experience.

Step 1: Analyze Current Failed Login Patterns

Data from 30 days (before lockout policy):
  Total login attempts: 450,000
  Failed logins: 18,000 (4% failure rate)
  Users with >3 failures: 427 users (14.2% of workforce)

Breakdown of failures:
  1-2 failures: 2,100 users (70% - typos, forgotten passwords)
  3-5 failures: 427 users (14.2% - legitimate users struggling)
  6+ failures: 73 users (2.4% - suspicious, possible brute force)

Step 2: Calculate Lockout Impact

// Scenario A: Aggressive Lockout (3 attempts, 30 minutes)
const int MAX_FAILED_ATTEMPTS = 3;
const unsigned long LOCKOUT_DURATION_MS = 1800000;  // 30 minutes

Impact Calculation:
  Users affected: 427 + 73 = 500 users/month (16.7%)
  Average lockout time: 30 minutes
  Productivity loss: 500 users × 30 min = 250 person-hours/month
  Help desk calls: 500 calls/month × $15/call = $7,500/month
  User frustration: HIGH

// Scenario B: Balanced Lockout (5 attempts, 15 minutes)
const int MAX_FAILED_ATTEMPTS = 5;
const unsigned long LOCKOUT_DURATION_MS = 900000;  // 15 minutes

Impact Calculation:
  Users affected: 73 users/month (2.4%)
  Average lockout time: 15 minutes
  Productivity loss: 73 × 15 min = 18.25 person-hours/month
  Help desk calls: 73 calls/month × $15/call = $1,095/month
  User frustration: LOW

// Scenario C: Lenient (10 attempts, 60 minutes)
const int MAX_FAILED_ATTEMPTS = 10;
const unsigned long LOCKOUT_DURATION_MS = 3600000;  // 60 minutes

Impact Calculation:
  Users affected: ~15 users/month (0.5%)
  Security risk: Brute force attacks can try 10 passwords
  Average lockout time: 60 minutes
  Productivity loss: 15 × 60 min = 15 person-hours/month
  Help desk calls: 15 calls/month × $15/call = $225/month
  Security posture: WEAK

Step 3: Implement Escalating Lockout

// BEST: Escalating lockout balances security and UX
unsigned long calculateLockoutDuration(int failedAttempts) {
    const unsigned long BASE_LOCKOUT = 60000;        // 1 minute
    const unsigned long ESCALATION_STEP = 30000;     // Add 30s per failure
    const unsigned long MAX_LOCKOUT = 300000;        // Cap at 5 minutes

    unsigned long duration = BASE_LOCKOUT +
        ((failedAttempts - MAX_FAILED_ATTEMPTS) * ESCALATION_STEP);

    return min(duration, MAX_LOCKOUT);
}

// Example progression:
Failures:  345678+
Lockout:  1m1.5m2m2.5m3m5m (capped)

Step 4: Measure Real-World Results After Implementation

After 30 days with escalating lockout (3 attempts, escalating):

Legitimate User Impact:
  Users locked out: 127 (down from 500)
  Average lockout: 1.8 minutes (down from 30)
  Help desk calls: 127 × $15 = $1,905/month (73% reduction)
  User satisfaction: 87% (was 62%)

Security Improvements:
  Brute force attempts detected: 47
  Accounts compromised: 0 (was 3 suspected)
  Average attacker attempts before lockout: 3.2
  Maximum attacker attempts: 8 (hit 5-minute cap)

ROI Calculation:
  Cost savings: $7,500 - $1,905 = $5,595/month
  Security incidents prevented: 3 compromises × $25,000 avg = $75,000
  Annual ROI: $67,140 + $75,000 = $142,140

Key Decisions Made:

  1. 3-attempt threshold: Catches obvious attacks while allowing typos
  2. Escalating duration: First lockout is short (1 min), grows with repeated failures
  3. 5-minute cap: Prevents excessive lockouts for confused legitimate users
  4. 30-second escalation steps: Gradual increase balances security and UX

Lesson Learned: Escalating lockouts provide better security than fixed timeouts while significantly improving user experience. The 73% reduction in help desk calls alone justified the implementation.

Use this calculator to explore how different lockout settings affect user experience and security costs for your deployment.

Choose the right authentication method based on your building’s security requirements and user population.

Factor RFID Card PIN Code Fingerprint Face Recognition
Security Level Medium Low High High
Cost per User $3-8 (card) $0 $15-30 (reader) $50-200 (reader)
Speed <1 second 3-5 seconds 1-2 seconds 1-3 seconds
User Acceptance Very High (95%+) High (85%) Medium (70%) Low (50%)
Hygiene Concerns None None High (COVID) None (contactless)
Lost/Stolen Risk High (card theft) Medium (shoulder surfing) Low (can’t steal) Low (can’t steal)
Works with Gloves Yes Yes No Yes
Privacy Concerns Low None Medium (biometric data) High (biometric + video)
Backup Method Required Yes (lost cards) No Yes (injury, sweat) Yes (poor lighting, masks)
ADA Compliance Excellent Good (audio prompts) Fair (tactile aids) Poor (blind users)

Use Case Recommendations:

Facility Type Primary Method Backup Method Rationale
Office (low security) RFID card PIN code Fast, cheap, high acceptance. PIN for lost cards.
Data Center (high security) Fingerprint + RFID PIN (admin override) Two-factor: something you have + something you are.
Hospital (hygiene priority) Face recognition RFID card No-touch for infection control. Card for masked staff.
Factory (gloves worn) RFID card Face recognition Gloves prevent fingerprint. Face works with PPE.
Apartment Building (residents) RFID fob + PIN Mobile app (Bluetooth) Low-cost. Residents unlikely to accept biometrics.

Decision Tree:

Q1: What is the security level required?
  Low (office, apartments) → RFID card + PIN
  High (data center, labs) → Continue to Q2

Q2: Are biometrics acceptable to users?
  NO → Multi-factor RFID (card + PIN)
  YES → Continue to Q3

Q3: Do users wear gloves or have hand injuries?
  YES → Face recognition + RFID backup
  NO → Fingerprint + RFID backup

Q4: Is contactless important (hygiene)?
  YES → Face recognition or mobile app (Bluetooth)
  NO → Fingerprint is most reliable biometric

Common Mistake: Choosing face recognition for “cool factor” without considering: - Privacy backlash (biometric data storage concerns) - Poor performance with masks, glasses, hats - High false rejection rate (5-10%) frustrates legitimate users - Expensive ($200+/reader vs $50 for RFID) - Requires backup method (adding cost and complexity)

Best Practice: Start with RFID + PIN (proven, cheap, high acceptance). Add biometrics only where security requirements justify the cost and user friction.

Common Mistake: Using strcmp() for Credential Comparison

Mistake: Using standard string comparison functions exposes your system to timing attacks.

Vulnerable Code:

// BAD: strcmp returns immediately on first mismatch
UserCredential* authenticateCard(const char* cardId) {
    for (int i = 0; i < NUM_USERS; i++) {
        if (strcmp(cardId, userDatabase[i].cardId) == 0) {
            return &userDatabase[i];  // FOUND!
        }
    }
    return NULL;  // Not found
}

Attack Scenario:

An attacker measures response times for different card IDs:

Attempt 1: cardId = "AAAA_ADMIN_001"
  Response time: 0.23 ms (fast - mismatched at first character 'A' vs 'R')

Attempt 2: cardId = "RFID_AAAA_001"
  Response time: 0.51 ms (slower - matched first 5 chars "RFID_")

Attempt 3: cardId = "RFID_ADMIN_001"
  Response time: 1.34 ms (even slower - matched 14 chars!)

By measuring timing differences, the attacker deduces: - Card IDs start with “RFID_” - Valid format is “RFID_ADMIN_NNN” - Can guess valid card IDs character-by-character

Why strcmp() is Vulnerable:

// Simplified strcmp implementation
int strcmp(const char* a, const char* b) {
    while (*a && (*a == *b)) {
        a++;
        b++;
    }
    return *(unsigned char*)a - *(unsigned char*)b;
}

The loop exits on the FIRST mismatch. If strings match for 5 characters, the loop runs 5 iterations. If they match for 10 characters, it runs 10 iterations. This timing difference leaks information!

Secure Implementation (from lab code):

// GOOD: Constant-time comparison
bool constantTimeCompare(const char* a, const char* b) {
    size_t lenA = strlen(a);
    size_t lenB = strlen(b);

    // Always process maximum length (timing independent of input)
    size_t maxLen = (lenA > lenB) ? lenA : lenB;

    volatile int result = 0;  // volatile prevents compiler optimization
    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;  // XOR accumulates differences
    }

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

    return (result == 0);  // True only if ALL bits matched
}

Why This Works:

  1. Fixed iterations: Always loops maxLen times, regardless of where mismatch occurs
  2. No early exit: Cannot break out of loop early (timing constant)
  3. XOR accumulation: Differences accumulate in result, but timing doesn’t reveal WHICH characters differ
  4. Volatile keyword: Prevents compiler from optimizing away comparisons

Timing Test Results:

Input Comparison              | strcmp() Time | constantTimeCompare() Time
------------------------------|---------------|---------------------------
"AAAA" vs "RFID_ADMIN_001"   | 0.18 ms      | 1.42 ms
"RFID" vs "RFID_ADMIN_001"   | 0.43 ms      | 1.42 ms
"RFID_ADMIN_00" vs "RFID_ADMIN_001" | 1.21 ms | 1.42 ms
"RFID_ADMIN_001" vs "RFID_ADMIN_001" | 1.34 ms | 1.42 ms

Standard deviation (strcmp):         0.47 ms (HUGE variance)
Standard deviation (constantTime):   0.03 ms (negligible)

Real-World Impact: In 2015, timing attacks were used to extract PayPal API keys character-by-character. The attack took 2 hours to extract a 32-character key by measuring response times. Constant-time comparison would have prevented this entirely.

Testing Checklist:


Matching Quiz: Match Basic Lab Components to Roles

Ordering Quiz: Order the Basic Authentication Lab Setup

Label the Diagram

Code Challenge

14.8 Deep Dive: Login Throttling and Offline Cracking

Strong password hashing protects credentials if the verifier database leaks. It does not stop an attacker from standing at the live login prompt and guessing. The implementation has to defend two separate attack surfaces: online guessing against the running service and offline cracking against a stolen verifier file.

Attack Surface Attacker Limit Primary Defences
Online guessing Service policy, rate limits, lockouts, network latency, telemetry, and MFA prompts. Per-account and per-source throttles, exponential backoff, step-up MFA, generic failure messages, and monitoring.
Offline cracking Attacker hardware and the cost of each KDF attempt. Unique salts, slow work factors, memory-hard KDF parameters, breached-password prevention, and credential reset after compromise.

The same password can face very different economics. Against the login page, the defender controls the clock: reject bursts, delay repeated failures, alert on suspicious sources, and require a second factor when risk rises. Against a leaked verifier table, the defender no longer controls the clock; the attacker can run GPUs or rented instances until guesses run out. That is why this lab treats the login handler, password verifier, and audit log as separate controls.

Visible feedback in a lab can also mislead if copied directly into production. A red LED or detailed error helps students understand the decision path, but a public service should return a bland failure while recording precise reasons internally. The user experience should remain recoverable for legitimate users, while monitoring still distinguishes typos, credential stuffing, targeted brute force, and account lockout abuse.

Rate limiting and lockout turn a fast online attack into a slow one:

Account lockout:
  after N failed attempts, lock or challenge for cooldown T

Exponential backoff:
  delay doubles after repeated failures
  1 s, 2 s, 4 s, 8 s, ...

Effective guess rate:
  attempts allowed per unit time

The arithmetic is the design check. If a policy allows five failed attempts per 15 minutes, an attacker gets about 480 guesses per day against one account. Exhausting a 4-digit PIN space of 10,000 combinations would take about 21 days in the worst case, or roughly 10 days on average, instead of seconds. If a policy allows three guesses and then a 10-minute cooldown, the cap is about 18 guesses per hour per account. These estimates are not enough by themselves, but they reveal whether the policy is in the right order of magnitude.

The rate limit should match the asset and the recovery path. A shared classroom lab can lock an account aggressively because an instructor can reset it. A production IoT fleet should not hard-lock every field technician after three failures if that blocks maintenance during an outage. Combine counters: per account to protect the user, per source network or device fingerprint to slow automation, and per credential pattern to catch password-spraying across many accounts. The policy should also define when to step up to MFA rather than simply refusing access.

Risk Safer Implementation Pattern
Lockout denial of service Prefer temporary delays, step-up challenges, and per-source-plus-account throttles over easily triggered hard lockouts.
Credential stuffing Watch for one guess across many accounts, use breached-password checks, MFA, and source/device reputation.
Username enumeration Return one generic failure message and keep response timing uniform; log the precise reason internally.
Counter reset by restart Store counters in a shared, durable rate-limit service or database rather than one process’s memory.
Overcollection in logs Log account identifier, method, source, result, and reason code; never log attempted passwords or bearer tokens.

Good implementations expire counters carefully so a single typo does not poison an account all day, and they avoid leaking state through messages such as “that email exists but is locked.” The safest public response is usually the same generic failure, while internal logs carry reason codes, counter values, IP or device signals, and whether a step-up challenge was offered. That split keeps the interface usable without turning it into an enumeration oracle.

Shelly’s Access Ledger

  • Who: throttles count per account, per source, and per credential pattern.
  • What: five failures per 15 minutes caps one account near 480 guesses a day; a 10,000-combination PIN survives ten days on average.
  • Proof: outside, one generic failure; inside, reason codes and counters — never the attempted password.

Throttle to stop guessing, but design the throttle so it cannot become a lockout weapon. The login prompt is defended by policy and rate, the database by hashing, and the account by MFA.

Shield ShellyCheckpoint: Production Boundaries

You now know which lab controls need production hardening:

  • The live login prompt is defended by throttles, lockouts, monitoring, and MFA; the leaked verifier database is defended by salts and slow KDF work factors.
  • Five failures per 15 minutes allows about 480 guesses per day, while three guesses and a 10-minute cooldown allows about 18 guesses per hour.
  • Public failures should stay generic, but internal logs should keep reason codes, counter values, source signals, and step-up decisions.

14.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 and authorization
Key Takeaway

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

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


14.10 Knowledge Check

Quiz: Lab: Access Control Implementation

Common Pitfalls

MD5 and SHA256 are cryptographic hash functions, not password hashing functions. They are fast (enabling brute force) and lack salt (enabling precomputed rainbow table attacks). Always use bcrypt, scrypt, or Argon2 for password storage.

Returning “User not found” vs “Password incorrect” tells attackers which usernames are valid. Always return the same generic error (“Invalid credentials”) regardless of whether the username or password was wrong.

Adding authorization checks directly inside each route handler leads to inconsistent enforcement — it’s easy to forget to add the check in a new handler. Implement authorization as Express middleware (or equivalent) that runs before every protected handler.

Standard string equality comparison (===) short-circuits on the first mismatch, creating a timing side-channel that can reveal how many characters of a guessed token match. Use crypto.timingSafeEqual() for all security-sensitive comparisons.

14.11 What’s Next

Continue your access control journey:

If you want to… Read this
Implement zero trust architecture Zero Trust Security
Understand threat modeling Threat Modelling and Mitigation
Try advanced access control concepts Advanced Access Control Concepts
Return to the lab overview Authentication and Access Control Overview
Practice with challenges Security Concepts & Challenges

Key Concepts

  • bcrypt: A password hashing function with built-in salt and adjustable cost factor; the standard for securely storing user passwords in databases
  • Password Salt: A random value added to the password before hashing; ensures that identical passwords produce different hashes, preventing rainbow table attacks
  • Protected Route: An API endpoint that requires a valid authentication token; implemented as middleware that rejects unauthorized requests before they reach the handler
  • Role Assignment: The process of associating a user or device with one or more roles that define their permissions; typically stored in the user record or a separate roles table
  • Permission Check: A validation that the authenticated principal’s role includes permission to perform the requested operation on the target resource
  • Timing-Safe Comparison: Using constant-time comparison functions for password verification to prevent timing attacks that can reveal whether a password attempt was close to correct
  • Token Expiry: The time-to-live value embedded in authentication tokens; balances security (short-lived = less exposure if stolen) against usability (long-lived = fewer re-logins)
In 60 Seconds

This hands-on lab builds a working RBAC system from scratch — implementing user authentication with hashed passwords, role assignment, permission checking middleware, and protected API endpoints — giving you practical experience with the security code patterns used in real IoT platforms.