Implementing User Action Audit in Corporate Mobile Apps
Security audit trail is not analytics or UX research. These are legally significant logs that will show during an incident: who, when, from which device opened a document, changed a record, exported a file. Without it, investigating a leak is impossible.
What Must Be Logged
The question isn't how to log — it's which events matter when analyzing an incident. Typical corporate minimum:
- Login and logout (including auto-logout by timeout)
- Access to documents or records with classification above "Internal"
- Data changes, creation, deletion
- Export, print, send — any data output beyond the app perimeter
- Failed authentication attempts (with counter)
- Security settings changes (PIN, biometry)
- Remote Wipe commands and their execution
Logging "user clicked back button" is not audit, it's noise.
Audit Trail Architecture
The key requirement for audit trail: logs must not be lost and must not be accessible for deletion by the user. These are two different technical requirements.
For reliable delivery — local queue with guaranteed sending. On Android — WorkManager with BackoffPolicy.EXPONENTIAL, on iOS — BGProcessingTask. Logs are written first to local SQLite table, then a background task sends them to server and deletes only after confirmation.
// Audit event model
data class AuditEvent(
val id: String = UUID.randomUUID().toString(),
val timestamp: Long = System.currentTimeMillis(),
val userId: String,
val deviceId: String,
val action: AuditAction,
val resourceId: String?,
val resourceType: String?,
val metadata: Map<String, String> = emptyMap(),
val synced: Boolean = false
)
enum class AuditAction {
LOGIN, LOGOUT, DOCUMENT_VIEW, DOCUMENT_EXPORT,
RECORD_CREATE, RECORD_UPDATE, RECORD_DELETE,
AUTH_FAILURE, SETTINGS_CHANGE, WIPE_RECEIVED
}
// DAO for local queue
@Dao
interface AuditEventDao {
@Insert
suspend fun insert(event: AuditEvent)
@Query("SELECT * FROM audit_events WHERE synced = 0 ORDER BY timestamp ASC LIMIT 50")
suspend fun getUnsynced(): List<AuditEvent>
@Query("UPDATE audit_events SET synced = 1 WHERE id IN (:ids)")
suspend fun markSynced(ids: List<String>)
}
Sync task runs when network appears and on app startup. Batch sending of 50 events — balance between server load and delivery speed.
Log Integrity
If the app runs on a device with root/jailbreak, the user could delete local SQLite. For high-level requirements, each event is signed with HMAC using a key from Android Keystore / iOS Secure Enclave:
fun signEvent(event: AuditEvent): String {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val privateKey = keyStore.getKey("audit_signing_key", null)
val signature = Signature.getInstance("SHA256withECDSA")
signature.initSign(privateKey as PrivateKey)
signature.update(event.toCanonicalBytes())
return Base64.encodeToString(signature.sign(), Base64.NO_WRAP)
}
Server verifies the signature with the public key. Forging a log without Secure Enclave access is impossible.
Context Enrichment
Plain userId + action + timestamp is the minimum. Useful additions:
-
deviceId— tie to specific device, not account -
appVersion— understand which version caused the incident -
networkType(WiFi/LTE/VPN) — see if corporate VPN was active -
jailbreak/root detected— flag from SafetyNet / DeviceCheck
On Android deviceId is Settings.Secure.ANDROID_ID (unique for device+user+app combo from Android 8). On iOS — UIDevice.current.identifierForVendor.
Server Storage
Audit logs — not something deleted after 30 days. Legal requirements (depending on industry): from 1 year (standard) to 7 years (financial organizations by Federal Law 115). Store in append-only storage — PostgreSQL with INSERT-only table and UPDATE/DELETE forbidden via Row Level Security, or separate SIEM (Splunk, ELK with ILM).
What to Check During App Audit
Often we find that the app already has "some logging" — but it writes to Logcat or file in cacheDir, which clears when storage runs low. This is not audit trail, it's garbage.
Timeline: 2–3 days for basic implementation with local queue and server endpoint. With HMAC signatures and SIEM integration — 4–6 days.







