18  Lab: Access Control Implementation

Complete Code with Testing Scenarios

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.

18.1 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:


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:


18.2 Complete Implementation Code

Copy this complete code into the Wokwi editor. 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.

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

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

18.2.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);

18.2.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);
}

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

18.2.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();
    }
}

18.2.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);
}

18.2.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");
    }
}

18.3 Testing Scenarios

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

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

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

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

18.3.5 Scenario E: Disabled Account

CARD 3
ZONE 0

Expected Results:

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


18.4 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


18.5 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


18.6 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:


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


18.8 Knowledge Check

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.

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