Implementing RFID Access Control in Mobile Applications
RFID access control via mobile app — not simply "read card and open door." It's audit log of every event, permission management for each user and zone, offline handling when door controller unavailable, and HID/Wiegand RFID reader integration. Reliability matters more than speed here.
Architecture: Where Logic Lives
Mobile app — administrator tool for access control systems and audit log viewer. "Open or not" decision must never be only on phone — door controller (e.g., HID VertX, Honeywell Pro-Watch) decides by its own database.
Typical flow:
Mobile app → REST API Access Control → Door Controller → Reader → Electromagnetic Lock
Mobile app manages:
- Card holder database (add, block, delete)
- Access schedule (when whom can enter which zone)
- Event monitoring in real time
- Manual door unlock (Remote Unlock)
Reading RFID Cards with Mobile Phone
Phone itself can act as RFID reader via NFC (for MIFARE Classic/DESFire cards) or via external BLE reader (for HF 13.56 MHz or LF 125 kHz cards).
NFC on iOS (CoreNFC) for MIFARE:
import CoreNFC
class AccessCardReader: NSObject, NFCTagReaderSessionDelegate {
var session: NFCTagReaderSession?
func startReading() {
session = NFCTagReaderSession(pollingOption: [.iso14443], delegate: self)
session?.alertMessage = "Apply access card"
session?.begin()
}
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
guard let tag = tags.first, case .miFare(let mifareTag) = tag else { return }
session.connect(to: tag) { error in
if error != nil { session.invalidate(errorMessage: "Connection error"); return }
// Read card UID (unique identifier)
let uid = mifareTag.identifier.map { String(format: "%02X", $0) }.joined()
self.onCardDetected(uid: uid)
session.invalidate()
}
}
}
MIFARE card UID — just unique number. For HID ProxCard 125 kHz — mobile phone cannot read physically. Need external BLE reader.
MIFARE DESFire EV2 — Secure Reading:
DESFire cards use in serious access control. Sector read requires AES-128 authentication:
// SELECT Application command
let selectCmd = Data([0x90, 0x5A, 0x00, 0x00, 0x03]) + applicationId + Data([0x00])
mifareTag.sendMiFareCommand(commandPacket: selectCmd) { response, error in
// Then Authenticate with AES key
}
Keys stored in Secure Enclave — not in app code. SecKeyCreateRandomKey with .secureEnclaveBound attribute.
Audit Log and Monitoring
Real-time event monitoring via WebSocket or SSE (Server-Sent Events):
class AccessEventMonitor(private val accessApi: AccessControlApi) {
private val _events = MutableSharedFlow<AccessEvent>(replay = 50)
val events: SharedFlow<AccessEvent> = _events.asSharedFlow()
fun startMonitoring(zoneIds: List<String>) {
scope.launch {
accessApi.streamEvents(zoneIds).collect { event ->
_events.emit(event)
if (event.accessResult == AccessResult.DENIED) {
sendDeniedAlert(event)
}
}
}
}
}
data class AccessEvent(
val cardholderName: String,
val cardUid: String,
val doorName: String,
val timestamp: Long,
val accessResult: AccessResult, // GRANTED / DENIED / TAILGATING
val deniedReason: String? // "outside_schedule", "card_blocked", "no_permission"
)
deniedReason — denial details. Guard needs to know: card blocked or person simply came outside schedule. Different actions — different response.
Remote Door Management
Remote Unlock — open door without physical presence:
suspend fun remoteUnlock(doorId: String, durationSeconds: Int = 5) {
val result = accessApi.unlockDoor(
doorId = doorId,
unlockDuration = durationSeconds,
operatorId = currentUser.id,
reason = "remote_unlock_mobile"
)
if (result.isSuccess) {
logAuditEvent(AuditAction.REMOTE_UNLOCK, doorId)
}
}
Every remote unlock — in audit log with operatorId. Without this, incident investigation impossible.
Timeline
Access control admin mobile app with audit log and REST integration to existing system: 5 days. With NFC MIFARE UID reading and Remote Unlock: +3 days. Full integration with HID or Honeywell access control including controller setup: 2–4 weeks.







