Skip to content

fix(#623): enforce identity verification on QR check-in to prevent sp…#646

Open
vedikathalkar wants to merge 1 commit into
roshankumar0036singh:mainfrom
vedikathalkar:fix/623-qr-checkin-identity-verification
Open

fix(#623): enforce identity verification on QR check-in to prevent sp…#646
vedikathalkar wants to merge 1 commit into
roshankumar0036singh:mainfrom
vedikathalkar:fix/623-qr-checkin-identity-verification

Conversation

@vedikathalkar

@vedikathalkar vedikathalkar commented Jun 16, 2026

Copy link
Copy Markdown

Fixes #623

Problem

QRScannerScreen.js submitted check-ins using the userId from the scanned QR payload without verifying it matched the authenticated user's uid. Any authenticated user could scan someone else's QR code and create ghost attendance records, inflating leaderboard scores.

Solution

  • Added identity check in handleBarCodeScanned: if scannedUserId !== user.uid, check-in is rejected with a clear error message before any Firestore write occurs
  • Added verifyAndCheckIn cloud function that enforces the same check server-side using context.auth.uid (which cannot be spoofed by the client)
  • Restricted direct client writes to the attendance subcollection in firestore.rules — all attendee check-ins must go through the cloud function

Files Changed

  • app/src/screens/QRScannerScreen.js — added identity verification before check-in submission
  • cloud-functions/src/verifyAndCheckIn.ts — new cloud function for server-side uid enforcement
  • cloud-functions/src/index.ts — exported the new cloud function
  • firestore.rules — restricted attendance writes with fix comment

Contributing under GSSoC '26

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed account-mismatch issue when scanning QR codes. Users can now only check in with QR codes assigned to their own account.
  • New Features

    • Added server-side verification for the check-in process to prevent duplicate check-ins and enhance security.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Addresses issue #623 by adding a three-layer identity check to the QR check-in flow: a new verifyAndCheckIn Cloud Function that validates caller auth matches the QR-encoded UID and writes attendance atomically, a client-side UID mismatch guard in QRScannerScreen, and Firestore rules that restrict direct attendance writes to admins and event owners.

Changes

Fix #623: QR Check-in Identity Verification

Layer / File(s) Summary
verifyAndCheckIn Cloud Function
cloud-functions/src/verifyAndCheckIn.ts, cloud-functions/src/index.ts
New HTTPS callable function enforces auth.uid === qrUid, validates payload, checks event existence and RSVP, handles duplicate check-ins idempotently, and writes attendance with reputation/count updates in an atomic Firestore transaction. Exported from the index barrel.
Client-side UID mismatch guard
app/src/screens/QRScannerScreen.js
handleBarCodeScanned gains an early-return gate that requires an authenticated user.uid and sets an error scanResult if scannedUserId !== user.uid, aborting before any backend call. Minor comment cleanup around scanResult state and the result modal section.
Firestore attendance write restriction and rules cleanup
firestore.rules
/events/{eventId}/attendance create/update/delete is now restricted to admins and event owners (not general attendees). getUserData helper is relocated earlier. Surrounding collection blocks (/users, /analytics, /reminders, /auditLog, /admin, /feedback) are reformatted with section comments removed; effective predicates are unchanged.

Sequence Diagram(s)

sequenceDiagram
  participant Scanner as QRScannerScreen
  participant CF as verifyAndCheckIn
  participant FS as Firestore

  Scanner->>Scanner: handleBarCodeScanned(qrData)
  alt user.uid !== scannedUserId
    Scanner-->>Scanner: set error scanResult, return early
  else UIDs match
    Scanner->>CF: call({ qrUid, eventId })
    CF->>CF: enforce auth.uid === qrUid
    CF->>FS: check event, RSVP, existing attendance
    alt already checked in
      CF-->>Scanner: { success: true, alreadyCheckedIn: true }
    else first check-in
      CF->>FS: atomic transaction (attendance + reputation update)
      CF-->>Scanner: { success: true, alreadyCheckedIn: false }
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • roshankumar0036singh/Uni-Event#248: Modifies the same handleBarCodeScanned function in QRScannerScreen.js, rerouting check-in through an offline queuing/sync service — directly overlapping with the auth validation gate added here.
  • roshankumar0036singh/Uni-Event#388: Also modifies handleBarCodeScanned in QRScannerScreen.js, changing ticketless/participant routing logic in the same check-in dispatch path that this PR adds a UID validation gate to.
  • roshankumar0036singh/Uni-Event#280: Modifies firestore.rules authorization predicates for /events/{eventId}/participants and related event subcollections, overlapping with the attendance write restriction changes in this PR.

Suggested labels

type:bug, level:intermediate, quality:clean, gssoc:approved

Suggested reviewers

  • roshankumar0036singh

Poem

🐰 Hippity-hop, no imposter shall pass!
The QR code thief gets an error — alas.
Cloud Functions now check that your UID is true,
Firestore rules lock the door — only YOU can get through.
The leaderboard's honest, attendance is fair,
No phantom check-ins floating in the air! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(#623): enforce identity verification on QR check-in to prevent sp…' is fully related to the main change. It accurately describes the primary objective of enforcing identity verification for QR code check-ins to prevent spoofing.
Linked Issues check ✅ Passed The pull request fully implements the requirements from issue #623: client-side identity verification in QRScannerScreen.js, server-side verification in verifyAndCheckIn cloud function, and Firestore security rule updates restricting direct attendance writes.
Out of Scope Changes check ✅ Passed All changes are directly related to addressing the identity verification vulnerability in issue #623. No out-of-scope changes are present; modifications to firestore.rules, cloud functions, and the QR scanner screen are all necessary for the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/623-qr-checkin-identity-verification

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

app/src/screens/QRScannerScreen.js

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

cloud-functions/src/index.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

cloud-functions/src/verifyAndCheckIn.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vedikathalkar

Copy link
Copy Markdown
Author

Hi @roshankumar0036singh, I've implemented the fix for this issue and raised a PR. Please review when you get a chance!

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/screens/QRScannerScreen.js (1)

259-275: ⚠️ Potential issue | 🔴 Critical

Fix line references: checkIns rules at 282–287 restrict direct writes; use verifyAndCheckIn cloud function instead.

The identity check passes, but submitCheckIn calls checkInAttendee/checkInParticipant which write directly to the events/{eventId}/checkIns/{userId} subcollection. The Firestore rules at lines 282–287 restrict checkIn writes to admins, event owners, and clubs with event ownership only.

For a regular attendee self-checking-in, this write will fail with permission-denied. The client should call the verifyAndCheckIn cloud function (which runs with admin privileges) instead of the direct-write service.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/screens/QRScannerScreen.js` around lines 259 - 275, The submitCheckIn
function calls checkInAttendee or checkInParticipant which write directly to the
events/{eventId}/checkIns/{userId} subcollection, but Firestore security rules
restrict these writes to admins and event owners only. For a regular attendee
self-checking-in, this direct write will fail with permission-denied errors.
Replace the submitCheckIn call with a call to the verifyAndCheckIn cloud
function instead, which runs with admin privileges and has the necessary
permissions to perform the check-in operation. Update the call at the point
where submitCheckIn is invoked to use verifyAndCheckIn with the appropriate
parameters instead.
🧹 Nitpick comments (1)
firestore.rules (1)

238-256: 💤 Low value

Redundant isClub() condition in write rules.

Line 252 checks isClub() && (isEventOwner(...) || isEventOwnerAfter(...)), but lines 253-254 also check isEventOwner(...) and isEventOwnerAfter(...) independently. The isClub() gate adds no additional restriction since event owners are permitted regardless of club status.

The condition can be simplified to: isAdmin() || isEventOwner(database, eventId) || isEventOwnerAfter(database, eventId).

This is a minor clarity issue; the current rules are functionally correct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@firestore.rules` around lines 238 - 256, The allow create, update, delete
rule in the attendance match block contains a redundant condition. The line
checking `isClub() && (isEventOwner(database, eventId) ||
isEventOwnerAfter(database, eventId))` is unnecessary because the subsequent
lines already independently check isEventOwner and isEventOwnerAfter, making the
isClub() gate redundant. Simplify the condition by removing this redundant
clause and consolidating the rules to: isAdmin() || isEventOwner(database,
eventId) || isEventOwnerAfter(database, eventId).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cloud-functions/src/verifyAndCheckIn.ts`:
- Around line 94-113: The tx.update call on the userRef will fail if the user
document does not exist yet, which is possible for new users. Although the code
correctly handles reading non-existent user data by defaulting reputationPoints
to 0, the subsequent tx.update will throw an error if that document is missing.
Replace the tx.update(userRef, {...}) call with tx.set(userRef, {...}, { merge:
true }) to ensure the operation succeeds whether the user document exists or
not. This way, the reputationPoints and attendanceCount fields will be created
if the document is new, or updated if it already exists.
- Around line 77-88: The duplicate check on attendanceRef outside the
transaction creates a race condition where two concurrent requests can both pass
the existence check before either writes. Move the attendanceRef.get() call and
the existence check (attendanceSnap.exists) inside the runTransaction callback
so that the read and write are atomic together. This ensures that when multiple
concurrent check-in requests arrive, only one can successfully write the
attendance record, while the others will see the existing document and fail
gracefully.

---

Outside diff comments:
In `@app/src/screens/QRScannerScreen.js`:
- Around line 259-275: The submitCheckIn function calls checkInAttendee or
checkInParticipant which write directly to the
events/{eventId}/checkIns/{userId} subcollection, but Firestore security rules
restrict these writes to admins and event owners only. For a regular attendee
self-checking-in, this direct write will fail with permission-denied errors.
Replace the submitCheckIn call with a call to the verifyAndCheckIn cloud
function instead, which runs with admin privileges and has the necessary
permissions to perform the check-in operation. Update the call at the point
where submitCheckIn is invoked to use verifyAndCheckIn with the appropriate
parameters instead.

---

Nitpick comments:
In `@firestore.rules`:
- Around line 238-256: The allow create, update, delete rule in the attendance
match block contains a redundant condition. The line checking `isClub() &&
(isEventOwner(database, eventId) || isEventOwnerAfter(database, eventId))` is
unnecessary because the subsequent lines already independently check
isEventOwner and isEventOwnerAfter, making the isClub() gate redundant. Simplify
the condition by removing this redundant clause and consolidating the rules to:
isAdmin() || isEventOwner(database, eventId) || isEventOwnerAfter(database,
eventId).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ea1ce1e7-b0a4-4f7a-8ea7-b4abb19dea66

📥 Commits

Reviewing files that changed from the base of the PR and between 100ceb3 and 06324a0.

📒 Files selected for processing (4)
  • app/src/screens/QRScannerScreen.js
  • cloud-functions/src/index.ts
  • cloud-functions/src/verifyAndCheckIn.ts
  • firestore.rules

Comment on lines +77 to +88
// ── 6. Prevent duplicate check-ins ────────────────────────────────────
const attendanceRef = db
.collection('events')
.doc(eventId)
.collection('attendance')
.doc(callerUid);
const attendanceSnap = await attendanceRef.get();

if (attendanceSnap.exists) {
// Already checked in – return success idempotently
return { success: true, alreadyCheckedIn: true };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

TOCTOU race: duplicate check is outside the transaction.

attendanceSnap is read at line 83, but the attendance write occurs inside runTransaction at line 101. Two concurrent check-in requests could both pass the existence check, then both enter the transaction. Since attendanceRef is not read inside the transaction, Firestore cannot detect the conflict—both writes may succeed, or one fails non-deterministically, and the reputation/attendance counters may double-increment.

Move the duplicate check inside the transaction to make it atomic.

🔧 Proposed fix
-    const attendanceSnap = await attendanceRef.get();
-
-    if (attendanceSnap.exists) {
-        // Already checked in – return success idempotently
-        return { success: true, alreadyCheckedIn: true };
-    }
-
-    // ── 7. Write attendance record & update reputation atomically ─────────
     const userRef = db.collection('users').doc(callerUid);
     const REPUTATION_POINTS = 10; // adjust to match your scoring schema

-    await db.runTransaction(async tx => {
+    const result = await db.runTransaction(async tx => {
+        const attendanceSnap = await tx.get(attendanceRef);
+        if (attendanceSnap.exists) {
+            return { alreadyCheckedIn: true };
+        }
+
         const userSnap = await tx.get(userRef);
         ...
+        return { alreadyCheckedIn: false };
     });

-    return { success: true, alreadyCheckedIn: false };
+    return { success: true, alreadyCheckedIn: result.alreadyCheckedIn };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ── 6. Prevent duplicate check-ins ────────────────────────────────────
const attendanceRef = db
.collection('events')
.doc(eventId)
.collection('attendance')
.doc(callerUid);
const attendanceSnap = await attendanceRef.get();
if (attendanceSnap.exists) {
// Already checked in – return success idempotently
return { success: true, alreadyCheckedIn: true };
}
// ── 6. Prevent duplicate check-ins ────────────────────────────────────
const attendanceRef = db
.collection('events')
.doc(eventId)
.collection('attendance')
.doc(callerUid);
// ── 7. Write attendance record & update reputation atomically ─────────
const userRef = db.collection('users').doc(callerUid);
const REPUTATION_POINTS = 10; // adjust to match your scoring schema
const result = await db.runTransaction(async tx => {
const attendanceSnap = await tx.get(attendanceRef);
if (attendanceSnap.exists) {
return { alreadyCheckedIn: true };
}
const userSnap = await tx.get(userRef);
// ... rest of transaction implementation
return { alreadyCheckedIn: false };
});
return { success: true, alreadyCheckedIn: result.alreadyCheckedIn };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/verifyAndCheckIn.ts` around lines 77 - 88, The duplicate
check on attendanceRef outside the transaction creates a race condition where
two concurrent requests can both pass the existence check before either writes.
Move the attendanceRef.get() call and the existence check
(attendanceSnap.exists) inside the runTransaction callback so that the read and
write are atomic together. This ensures that when multiple concurrent check-in
requests arrive, only one can successfully write the attendance record, while
the others will see the existing document and fail gracefully.

Comment on lines +94 to +113
await db.runTransaction(async tx => {
const userSnap = await tx.get(userRef);
const currentPoints: number = userSnap.exists
? (userSnap.data()?.reputationPoints ?? 0)
: 0;

// Mark attendance
tx.set(attendanceRef, {
uid: callerUid,
eventId,
checkedInAt: admin.firestore.FieldValue.serverTimestamp(),
verifiedByServer: true, // audit flag – distinguishes server-verified records
});

// Increment reputation
tx.update(userRef, {
reputationPoints: currentPoints + REPUTATION_POINTS,
attendanceCount: admin.firestore.FieldValue.increment(1),
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

tx.update will fail if the user document does not exist.

Line 96 handles the case where userSnap does not exist for reading reputationPoints, but line 109 uses tx.update(userRef, ...). Firestore update throws if the document is missing. For new users who haven't written a profile yet, this will crash the transaction.

Use tx.set with { merge: true } to handle both existing and non-existing user documents.

🔧 Proposed fix
-        // Increment reputation
-        tx.update(userRef, {
-            reputationPoints: currentPoints + REPUTATION_POINTS,
-            attendanceCount: admin.firestore.FieldValue.increment(1),
-        });
+        // Increment reputation (merge handles missing user docs)
+        tx.set(userRef, {
+            reputationPoints: currentPoints + REPUTATION_POINTS,
+            attendanceCount: admin.firestore.FieldValue.increment(1),
+        }, { merge: true });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/verifyAndCheckIn.ts` around lines 94 - 113, The tx.update
call on the userRef will fail if the user document does not exist yet, which is
possible for new users. Although the code correctly handles reading non-existent
user data by defaulting reputationPoints to 0, the subsequent tx.update will
throw an error if that document is missing. Replace the tx.update(userRef,
{...}) call with tx.set(userRef, {...}, { merge: true }) to ensure the operation
succeeds whether the user document exists or not. This way, the reputationPoints
and attendanceCount fields will be created if the document is new, or updated if
it already exists.

@roshankumar0036singh

Copy link
Copy Markdown
Owner

resolev the usggestiosn and conflict

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] QR code check-in accepts scan from any authenticated user attendee identity is never verified

3 participants