Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Implement Facial Recognition for Enhanced Security in FlutterFlow

Hardening facial recognition for production security requires four additions on top of basic face matching: liveness detection (blink or head-turn challenge to defeat printed photos), rate limiting (3 failed attempts triggers a 30-minute Firestore-backed lockout), multi-factor requirement (face can never be the sole authentication factor), and a complete audit trail logging every attempt with device metadata to Firestore.

What you'll learn

  • How to implement liveness detection challenges to defeat photo and screen spoofing attacks
  • How to enforce a server-side lockout after 3 failed face authentication attempts
  • How to integrate face recognition as a second factor alongside password authentication
  • How to build a Firestore-backed audit trail for every face authentication event
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate13 min read45-60 minFlutterFlow Pro+ (Cloud Functions, Firestore, and custom Dart required)March 2026RapidDev Engineering Team
TL;DR

Hardening facial recognition for production security requires four additions on top of basic face matching: liveness detection (blink or head-turn challenge to defeat printed photos), rate limiting (3 failed attempts triggers a 30-minute Firestore-backed lockout), multi-factor requirement (face can never be the sole authentication factor), and a complete audit trail logging every attempt with device metadata to Firestore.

Security Hardening for Face Recognition Authentication

Basic face recognition matching (covered in the enrollment and authentication tutorial) has known vulnerabilities: printed photos, phone screen photos, and even video playback can fool naive matching systems. This tutorial adds four security layers that address these attack vectors. Each layer is independent and can be adopted separately, but together they meet the security bar required for apps handling sensitive data, financial transactions, or regulated information.

Prerequisites

  • Completed the 'How to Implement Face Recognition for User Authentication in FlutterFlow' tutorial
  • Firebase Firestore with users collection and faceEmbedding field already set up
  • Firebase Cloud Functions deployed for face embedding extraction
  • Understanding of FlutterFlow Custom Actions and Action Flows

Step-by-step guide

1

Add Liveness Challenge Screen Before Face Capture

Liveness detection requires the user to perform a specific action — blinking, turning their head, or smiling — to prove a live person is present rather than a photo. In FlutterFlow, create a new page called 'LivenessChallenge'. It shows the front camera feed (via a Custom Widget using camera package) and displays a text instruction like 'Please blink twice' or 'Turn your head slowly to the right'. Each time the page loads, randomly select one challenge from three types stored in an App State list. After the user completes the challenge, navigate to the main face capture. You can validate the challenge with basic ML Kit pose/landmark detection — if the detected face landmarks show the expected movement (eyes closed, head rotation angle changed by 15+ degrees), the liveness check passes. Wire the Liveness Challenge page as the required step before the authenticateWithFace Custom Action.

perform_liveness_check.dart
1// Custom Action: performLivenessCheck
2// Return type: bool
3// Packages: camera, google_ml_kit (face detection)
4
5Future<bool> performLivenessCheck(String challengeType) async {
6 // challengeType: 'blink' | 'headTurnLeft' | 'headTurnRight' | 'smile'
7 final cameras = await availableCameras();
8 final frontCamera = cameras.firstWhere(
9 (c) => c.lensDirection == CameraLensDirection.front,
10 orElse: () => cameras.first,
11 );
12
13 final controller = CameraController(
14 frontCamera,
15 ResolutionPreset.medium,
16 enableAudio: false,
17 );
18 await controller.initialize();
19
20 bool challengePassed = false;
21 int frameCount = 0;
22 const maxFrames = 90; // 3 seconds at 30fps
23
24 final faceDetector = FaceDetector(
25 options: FaceDetectorOptions(
26 enableClassification: true, // for blink/smile
27 enableContours: true,
28 enableLandmarks: true,
29 enableTracking: true,
30 performanceMode: FaceDetectorMode.accurate,
31 ),
32 );
33
34 await controller.startImageStream((CameraImage image) async {
35 if (frameCount++ > maxFrames || challengePassed) return;
36
37 // Convert to InputImage and detect faces
38 // (Platform-specific byte conversion omitted for brevity)
39 // final faces = await faceDetector.processImage(inputImage);
40 // if (faces.isEmpty) return;
41 // final face = faces.first;
42
43 // Validate challenge based on type
44 // if (challengeType == 'blink') {
45 // challengePassed =
46 // (face.leftEyeOpenProbability ?? 1.0) < 0.2 &&
47 // (face.rightEyeOpenProbability ?? 1.0) < 0.2;
48 // } else if (challengeType == 'smile') {
49 // challengePassed = (face.smilingProbability ?? 0.0) > 0.8;
50 // } else if (challengeType == 'headTurnLeft') {
51 // challengePassed = (face.headEulerAngleY ?? 0.0) < -20;
52 // } else if (challengeType == 'headTurnRight') {
53 // challengePassed = (face.headEulerAngleY ?? 0.0) > 20;
54 // }
55 });
56
57 await Future.delayed(const Duration(seconds: 3));
58 await controller.stopImageStream();
59 await controller.dispose();
60 await faceDetector.close();
61
62 return challengePassed;
63}

Expected result: The liveness challenge page appears before face capture. Submitting a static photo of a face fails the challenge. A live user completing the requested action passes.

2

Implement Server-Side Lockout in Firestore

Store lockout state in Firestore, not App State — App State resets when the app is force-closed. In your Firestore users collection, add two fields: 'faceAuthFailedAttempts' (Number, default 0) and 'faceAuthLockedUntil' (Timestamp, nullable). In the authentication Custom Action, before attempting face matching, read these fields from the user's document. If lockedUntil is set and is in the future, return immediately with an error message showing remaining lockout time. If the face match fails, increment faceAuthFailedAttempts using Firestore's FieldValue.increment(1). If the new count reaches 3, set faceAuthLockedUntil to Timestamp.fromDate(DateTime.now().add(Duration(minutes: 30))) and reset faceAuthFailedAttempts to 0. On success, reset both fields to their defaults.

authenticate_with_face_secure.dart
1// Custom Action: authenticateWithFaceSecure
2// Return type: String (success, locked, failed, error)
3// Packages: cloud_firestore, firebase_auth, image_picker
4
5Future<String> authenticateWithFaceSecure() async {
6 final uid = FirebaseAuth.instance.currentUser?.uid;
7 if (uid == null) return 'error:not_authenticated';
8
9 // 1. Check lockout status server-side
10 final userDoc = await FirebaseFirestore.instance
11 .collection('users').doc(uid).get();
12 final data = userDoc.data()!;
13
14 final lockedUntilTs = data['faceAuthLockedUntil'] as Timestamp?;
15 if (lockedUntilTs != null) {
16 final lockedUntil = lockedUntilTs.toDate();
17 if (DateTime.now().isBefore(lockedUntil)) {
18 final remaining = lockedUntil.difference(DateTime.now());
19 final minutes = remaining.inMinutes + 1;
20 return 'locked:Face authentication locked for $minutes more minutes';
21 } else {
22 // Lockout expired — clear it
23 await FirebaseFirestore.instance
24 .collection('users').doc(uid).update({
25 'faceAuthLockedUntil': FieldValue.delete(),
26 'faceAuthFailedAttempts': 0,
27 });
28 }
29 }
30
31 // 2. Capture photo and extract embedding
32 final picker = ImagePicker();
33 final photo = await picker.pickImage(
34 source: ImageSource.camera,
35 preferredCameraDevice: CameraDevice.front,
36 imageQuality: 85,
37 );
38 if (photo == null) return 'cancelled';
39
40 final bytes = await photo.readAsBytes();
41 final base64Image = base64Encode(bytes);
42
43 try {
44 final callable = FirebaseFunctions.instance
45 .httpsCallable('extractFaceEmbedding');
46 final result = await callable.call({'imageBase64': base64Image});
47 final liveEmbedding = List<double>.from(
48 (result.data['embedding'] as List).map((e) => (e as num).toDouble()),
49 );
50 final storedEmbedding = List<double>.from(
51 (data['faceEmbedding'] as List).map((e) => (e as num).toDouble()),
52 );
53
54 final similarity = _cosineSimilarity(liveEmbedding, storedEmbedding);
55
56 if (similarity >= 0.85) {
57 // Success — reset failure count
58 await FirebaseFirestore.instance
59 .collection('users').doc(uid).update({
60 'faceAuthFailedAttempts': 0,
61 'faceAuthLockedUntil': FieldValue.delete(),
62 });
63 await _logFaceAuthEvent(uid, 'success', similarity);
64 return 'success';
65 } else {
66 // Failure — increment count, check lockout threshold
67 final newCount =
68 ((data['faceAuthFailedAttempts'] as num?)?.toInt() ?? 0) + 1;
69 final updates = <String, dynamic>{
70 'faceAuthFailedAttempts': newCount,
71 };
72 if (newCount >= 3) {
73 updates['faceAuthLockedUntil'] = Timestamp.fromDate(
74 DateTime.now().add(const Duration(minutes: 30)),
75 );
76 updates['faceAuthFailedAttempts'] = 0;
77 }
78 await FirebaseFirestore.instance
79 .collection('users').doc(uid).update(updates);
80 await _logFaceAuthEvent(uid, 'failed', similarity);
81 return newCount >= 3
82 ? 'locked:Too many failed attempts. Locked for 30 minutes.'
83 : 'failed:Face not recognized. ${3 - newCount} attempt(s) remaining.';
84 }
85 } on FirebaseFunctionsException catch (e) {
86 await _logFaceAuthEvent(uid, 'error', 0);
87 return 'error:${e.message}';
88 }
89}

Expected result: After 3 failed face authentication attempts, the Firestore document shows faceAuthLockedUntil set 30 minutes in the future. The action returns a 'locked' response on subsequent calls until the time expires.

3

Enforce Face as Second Factor Only

Configure your authentication flow to require email/password login FIRST, and only then present the face authentication challenge. In FlutterFlow's Action Flow on your login button: (1) Action 1: Log In (Firebase Auth with email and password) — on success, check if user has faceEnrolled=true. (2) If faceEnrolled is true, set a Page State variable 'requiresFaceAuth' to true and navigate to a 'Face Verification' interstitial page instead of the home page. (3) On the Face Verification page, run the authenticateWithFaceSecure Custom Action. Only navigate to Home on 'success'. Add a 'Skip face verification' option that sends a re-verification email and temporarily disables face auth for the current session — this is the recovery path for users who cannot pass face auth (new glasses, injury, etc.).

Expected result: Users must complete email/password login before face authentication is even presented. Bypassing the face auth page directly to home is impossible without a valid session from step one.

4

Build the Firestore Audit Trail

Create a 'face_auth_events' Firestore collection. Each document records: 'userId' (String), 'eventType' (String: 'success', 'failed', 'locked', 'enrolled', 'unenrolled'), 'similarity' (Number, the cosine similarity score), 'timestamp' (Timestamp), 'deviceModel' (String, from device_info_plus package), 'ipAddress' (String, from Cloud Function), and 'sessionId' (String, a UUID generated per login attempt). The _logFaceAuthEvent helper in the previous step writes to this collection. Build a simple 'Security Events' page in FlutterFlow (Admin only, protected by role-based access control) that shows this collection in a Repeating Group with date filtering. This gives you visibility into suspicious patterns like many failed attempts from multiple devices.

log_face_auth_event.dart
1// Helper: _logFaceAuthEvent (used inside Custom Actions)
2// Call this after every face authentication outcome
3
4Future<void> _logFaceAuthEvent(
5 String uid,
6 String eventType,
7 double similarity,
8) async {
9 String deviceModel = 'unknown';
10 try {
11 final info = DeviceInfoPlugin();
12 if (Platform.isAndroid) {
13 final android = await info.androidInfo;
14 deviceModel = '${android.manufacturer} ${android.model}';
15 } else if (Platform.isIOS) {
16 final ios = await info.iosInfo;
17 deviceModel = ios.model;
18 }
19 } catch (_) {}
20
21 await FirebaseFirestore.instance
22 .collection('face_auth_events')
23 .add({
24 'userId': uid,
25 'eventType': eventType,
26 'similarity': similarity,
27 'deviceModel': deviceModel,
28 'timestamp': FieldValue.serverTimestamp(),
29 'sessionId': DateTime.now().millisecondsSinceEpoch.toString(),
30 });
31}

Expected result: Every face authentication attempt creates a document in face_auth_events with the outcome, similarity score, and device model. Admin security page shows a timeline of all events.

Complete working example

face_auth_security.dart
1// ============================================================
2// FlutterFlow Face Recognition — Security Hardening
3// ============================================================
4// Cosine similarity helper (used by auth actions)
5double _cosineSimilarity(List<double> a, List<double> b) {
6 double dot = 0, normA = 0, normB = 0;
7 for (int i = 0; i < a.length; i++) {
8 dot += a[i] * b[i];
9 normA += a[i] * a[i];
10 normB += b[i] * b[i];
11 }
12 if (normA == 0 || normB == 0) return 0.0;
13 return dot / (sqrt(normA) * sqrt(normB));
14}
15
16// Audit logging helper
17Future<void> _logFaceAuthEvent(
18 String uid, String eventType, double similarity) async {
19 String deviceModel = 'unknown';
20 try {
21 if (Platform.isAndroid) {
22 final info = await DeviceInfoPlugin().androidInfo;
23 deviceModel = '${info.manufacturer} ${info.model}';
24 } else if (Platform.isIOS) {
25 final info = await DeviceInfoPlugin().iosInfo;
26 deviceModel = info.model;
27 }
28 } catch (_) {}
29 await FirebaseFirestore.instance.collection('face_auth_events').add({
30 'userId': uid,
31 'eventType': eventType,
32 'similarity': similarity,
33 'deviceModel': deviceModel,
34 'timestamp': FieldValue.serverTimestamp(),
35 });
36}
37
38// Lockout check helper — returns null if not locked, or lock message
39Future<String?> _checkLockout(String uid) async {
40 final doc = await FirebaseFirestore.instance
41 .collection('users').doc(uid).get();
42 final ts = doc.data()?['faceAuthLockedUntil'] as Timestamp?;
43 if (ts == null) return null;
44 final until = ts.toDate();
45 if (DateTime.now().isBefore(until)) {
46 final remaining = until.difference(DateTime.now()).inMinutes + 1;
47 return 'Account locked for $remaining more minute(s). Try again later.';
48 }
49 // Lockout expired — clean up
50 await FirebaseFirestore.instance.collection('users').doc(uid).update({
51 'faceAuthLockedUntil': FieldValue.delete(),
52 'faceAuthFailedAttempts': 0,
53 });
54 return null;
55}
56
57// Record a failure and apply lockout if threshold reached
58Future<void> _recordFailure(String uid) async {
59 final ref = FirebaseFirestore.instance.collection('users').doc(uid);
60 final doc = await ref.get();
61 final currentCount =
62 ((doc.data()?['faceAuthFailedAttempts'] as num?)?.toInt() ?? 0) + 1;
63 final updates = <String, dynamic>{
64 'faceAuthFailedAttempts': currentCount
65 };
66 if (currentCount >= 3) {
67 updates['faceAuthLockedUntil'] = Timestamp.fromDate(
68 DateTime.now().add(const Duration(minutes: 30)),
69 );
70 updates['faceAuthFailedAttempts'] = 0;
71 }
72 await ref.update(updates);
73}

Common mistakes

Why it's a problem: Using face recognition as the ONLY authentication factor

How to avoid: Always require face recognition as a SECOND factor alongside something the user knows (password) or has (OTP code). The face factor supplements security — it never replaces the first factor. Always provide an account recovery path that does not rely solely on face auth.

Why it's a problem: Storing lockout state only in App State instead of Firestore

How to avoid: Write lockout state (failedAttempts count and lockedUntil timestamp) to the user's Firestore document. Read and enforce lockout from Firestore at the START of every authentication attempt, before any face capture occurs.

Why it's a problem: Not providing a bypass path for users who cannot complete face authentication

How to avoid: Always provide an alternative authentication path: 'Trouble with face recognition? Verify your identity by email.' This sends a one-time code to the registered email and grants session access after verification, while flagging the account for re-enrollment.

Why it's a problem: Skipping audit logging to save Firestore write costs

How to avoid: Always log every authentication event (success and failure) with timestamp, deviceModel, and outcome. Firestore write costs for an audit log are minimal — even 10,000 auth events per day costs about $0.10 in Firestore writes.

Why it's a problem: Displaying the cosine similarity score in a debug label visible to end users

How to avoid: Log similarity scores to the server-side audit trail only. Show users only pass/fail outcomes. Remove any debug Text widgets showing similarity values before production deployment.

Best practices

  • Implement all four security layers together — liveness, lockout, multi-factor, and audit — for a defense-in-depth approach rather than relying on any single measure.
  • Choose liveness challenges randomly from a set of 3-4 options so attackers cannot pre-record a specific response to replay.
  • Set lockout duration to 30 minutes for 3 failures — short enough to not frustrate legitimate users, long enough to prevent automated attacks.
  • Send a security email notification to the user's registered email when a lockout is triggered — this alerts legitimate users to suspicious access attempts.
  • Review audit logs weekly using a Cloud Function or Firestore scheduled query — look for accounts with unusually high failure rates indicating targeted attacks.
  • Re-prompt for face enrollment if the last enrollment was over 180 days ago — face appearance changes significantly over time and stale embeddings cause false rejections.
  • Consult RapidDev for production deployments handling sensitive financial or medical data — biometric authentication systems require security reviews and may have regulatory requirements beyond what this tutorial covers.
  • Never log the actual face embedding or image to the audit trail — embeddings are biometric data subject to strict privacy regulations in most jurisdictions.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a production face recognition authentication system in FlutterFlow backed by Firebase. I need to add: (1) server-side lockout after 3 failed attempts stored in Firestore, (2) audit trail logging every attempt with outcome and device info, and (3) enforcement that face auth is always second-factor only after password login. Write the Dart Custom Action code for the secure authentication flow including lockout check, failure recording, and audit logging.

FlutterFlow Prompt

Write a FlutterFlow Custom Action in Dart called performLivenessCheck that takes a challengeType parameter ('blink', 'smile', 'headTurnLeft', 'headTurnRight'), uses the google_ml_kit face detection package to analyze a 3-second camera stream, and returns true if the face performs the requested challenge during the observation window.

Frequently asked questions

Can liveness detection completely prevent spoofing attacks?

No single liveness technique is completely foolproof. Blink and head-turn detection defends against static photos. It is less effective against high-quality video replays or 3D face models. For the highest security, combine multiple liveness challenges, use 3D depth sensing if the device supports it (iPhone Face ID hardware), and implement rate limiting to prevent automated attack attempts.

What GDPR requirements apply to storing face authentication data?

Under GDPR, face images and face embeddings are 'biometric data' — a special category requiring explicit informed consent, a specific legal basis (typically consent or legitimate interest), data minimization (store embeddings, not images), the right to erasure on request, and a Data Protection Impact Assessment (DPIA) for high-risk processing. Consult a legal professional before deploying biometric authentication in EU jurisdictions.

How does the lockout prevent attackers if they can just create a new account?

The lockout is per-account, targeting attackers who have the victim's email/password (first factor) and are trying to bypass face authentication (second factor). Creating a new account would not help because they would need the victim's account. The lockout is defense against the specific attack vector of an adversary who has stolen first-factor credentials.

Should I notify users by push notification when face auth is locked?

Yes — sending an email (not just push notification, since the device may have been stolen) when an account lockout triggers alerts legitimate users that someone is attempting unauthorized access. Include the device model from the audit log and instructions to change their password if they did not initiate the attempts.

What happens to the audit trail when a user deletes their account?

Under GDPR's right to erasure, you should delete face_auth_events documents linked to the user's ID when they delete their account. A Cloud Function triggered by the Firestore user document deletion can cascade-delete all associated audit records. However, you may retain anonymized aggregate statistics for fraud detection purposes.

Is a 0.85 cosine similarity threshold appropriate for all user populations?

The appropriate threshold depends on your face embedding model's discriminative power. For Cloud Vision landmark-based embeddings (used in this tutorial series), 0.85 is a reasonable starting point. For better-performing dedicated models like FaceNet, a Euclidean distance threshold under 1.0 is typical. Tune the threshold by testing with a diverse set of users and measuring false acceptance rate (FAR) and false rejection rate (FRR).

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.