-
Notifications
You must be signed in to change notification settings - Fork 30
6. Authentication using Firebase
Firebase Authentication serves as the sole identity and access layer for this platform. Rather than operating a custom authentication server or adopting a session-based middleware approach, the project delegates all identity concerns — user creation, credential verification, token issuance, and session persistence — to Firebase's managed authentication service. This decision eliminates the operational burden of password hashing, token rotation, and secure storage that would otherwise fall to the application team. The platform supports three authentication providers: email and password, Google OAuth via browser popup, and GitHub OAuth via browser popup.
Firebase Authentication issues RS256-signed JSON Web Tokens with a one-hour time-to-live. The client SDK refreshes these tokens automatically and silently in the background, meaning the application always has access to a valid credential for the currently signed-in user without any manual token management. Each token carries the user's unique identifier and a set of standard claims that the rest of the Firebase platform uses to authorise data access.
The integration with the Next.js frontend is entirely client-side. There is no server-side session, no HTTP-only cookie, and no middleware that inspects request headers. Authentication state lives in the browser, persisted to localStorage by default so that it survives page refreshes and tab closures. When the application loads, Firebase reads the persisted state and asynchronously re-establishes the authenticated session via an observable listener, briefly rendering in a loading state on first mount before handing the resolved user object to subscribers. The application consumes this observable through the useAuthState hook from the react-firebase-hooks library, which wraps Firebase's underlying auth listener and exposes it as a React-idiomatic tuple containing the current user object, a loading flag, and an error value. From the perspective of the Next.js application, Firebase Authentication therefore behaves as a reactive, observable data source: components subscribe to auth state changes and re-render when the signed-in user changes.
This purely client-side model is consistent with the project's architecture. All data access is directed through Firestore; because no server-side Security Rules are deployed in this repository, authorisation is enforced entirely at the client layer.
Email and password is the foundational, credential-based provider. During sign-up, the user supplies an email address and a password; Firebase validates the uniqueness of the address, hashes the password using a strong algorithm managed entirely on Firebase's servers, and creates a new user account. On success, Firebase immediately signs the user in and emits an auth state change, so the sign-up flow and the sign-in flow resolve to the same outcome: an authenticated session. For returning users, the sign-in path accepts the same email and password pair and verifies it against the stored credential. A password reset flow is also supported: the user submits their email address and Firebase dispatches a reset link to that address, allowing them to set a new password without knowing the old one.
sequenceDiagram
participant User
participant Modal as Auth Modal
participant Firebase as Firebase Auth
User->>Modal: Submits email + password (sign-up or sign-in)
Modal->>Firebase: createUserWithEmailAndPassword or signInWithEmailAndPassword
Firebase-->>Firebase: Validate credentials / create account
Firebase-->>Modal: Returns UserCredential
Modal-->>User: Auth state emitted → modal closes
Google and GitHub are both OAuth 2.0-based providers. Rather than the user supplying a password to the platform, they are redirected (via a browser popup) to the identity provider's own consent screen, where they authenticate with that provider and grant the platform permission to read their basic profile information. The identity provider then returns an OAuth token to Firebase, which exchanges it for a Firebase session. On a user's first sign-in with an OAuth provider, Firebase automatically creates a new account — there is no separate registration step required. The platform never handles a password at all — the credential lives entirely with Google or GitHub.
sequenceDiagram
participant User
participant Modal as Auth Modal
participant Firebase as Firebase Auth
participant Provider as Google / GitHub
User->>Modal: Clicks "Continue with Google" or "Continue with GitHub"
Modal->>Firebase: signInWithPopup(provider)
Firebase->>Provider: Opens provider consent popup
User->>Provider: Authenticates and grants consent
Provider-->>Firebase: Returns OAuth token
Firebase-->>Firebase: Verifies token, creates or links account
Firebase-->>Modal: Returns UserCredential
Modal-->>User: Auth state emitted → modal closes
The two categories of provider — credential-based and OAuth-based — differ primarily in where trust is established. With email and password, the platform's Firebase project is the authority. With OAuth, trust is delegated to a third-party identity provider. Both paths ultimately produce the same Firebase user identity and the same JWT.
The foundation of authentication state in this application is Firebase's onAuthStateChanged listener — the underlying observable that fires every time the authentication state changes. The application does not call this listener directly; instead it uses the useAuthState hook from the react-firebase-hooks library, which wraps the listener and exposes it as a React-idiomatic tuple of three values: the current user object (or null if unauthenticated), a boolean loading flag that is true while the initial session check is in progress, and an error value for any failure during that check.
The user-facing entry point for authentication is a global modal driven by a Jotai atom. This atom holds two pieces of state: whether the modal is currently open, and which view — login, sign-up, or password reset — should be displayed. When an unauthenticated user attempts a protected action such as voting on a post, joining a community, or creating new content, the action handler reads the current auth state and, finding no user, opens the modal by writing to this atom. The modal itself subscribes to the auth state, and when Firebase reports a successful sign-in — regardless of which provider was used — the modal closes automatically. No explicit close call is needed in any of the individual sign-in flows.
Beyond the modal, the resolved auth state drives a broader set of side effects through a headless bootstrap component that sits in the layout. This component listens to authentication state transitions: on sign-in, it populates community membership data and saved-post records into the Jotai atom graph; on sign-out, it clears this derived state. These two operations together ensure that the user's personalised data is always synchronised with their current session.
stateDiagram-v2
[*] --> Loading : App mounts / page refresh
Loading --> Unauthenticated : onAuthStateChanged fires (no user)
Loading --> Authenticated : onAuthStateChanged fires (user restored)
Unauthenticated --> ModalOpen : User triggers protected action
ModalOpen --> ModalOpen : User switches view (login ↔ sign-up ↔ reset)
ModalOpen --> Unauthenticated : User dismisses modal
ModalOpen --> Authenticated : Sign-in / sign-up succeeds
Authenticated --> Unauthenticated : User signs out
Because persistence is set to the LOCAL mode, the auth state survives across browser sessions. When a user returns to the application after closing their browser, Firebase reads the stored session from localStorage and fires the auth state observer with the restored user before the loading flag clears.
User identity in this platform is anchored to the Firebase Authentication user object rather than a dedicated Firestore document. The core identity attributes — display name and profile photo URL — are properties of the Auth user itself.
A separate Firestore user document is created automatically when an account is first registered, via a Cloud Function that fires on the Auth account-creation event. This document stores a copy of the Firebase User object at the time of account creation and serves as a user record for features that need to reference user attributes.
When a user uploads a new profile image, the image binary is written to Firebase Storage. Once the upload completes, the resulting download URL is retrieved and written back to the Firebase Auth user's photo URL field.
When a name change is committed, the platform performs three concurrent operations: the Firebase Auth user object is updated with the new display name; all Firestore documents representing posts authored by that user are patched to reflect the new name; and all Firestore documents representing comments authored by that user are similarly updated. In addition, the in-memory Jotai state is patched immediately so the UI reflects the change without requiring a data refetch.
flowchart TD
A[User submits profile update] --> B{What changed?}
B --> |Photo| C[Upload image to Firebase Storage]
C --> D[Retrieve download URL]
D --> E[Write URL to Auth user photo field]
E --> J[Profile update complete]
B --> |Display name| F[Update Auth user display name]
F --> G[Patch authored posts in Firestore]
F --> H[Patch authored comments in Firestore]
F --> I[Update Jotai in-memory state]
G --> J
H --> J
I --> J
Important: All access-control checks in this project are enforced entirely on the client side. No Firestore Security Rules are deployed, meaning the Firestore database itself is not protected at the server layer. The controls described below are user-experience guards only and can be bypassed by a client that communicates with Firestore directly.
A community is represented by a top-level Firestore document containing the creator's user identifier, the community's privacy type, a running count of members, and an array of admin user identifiers. Membership is stored separately in each user's subcollection.
Communities are classified into three privacy tiers that govern what visitors and authenticated users may do:
- Public: fully open — any visitor may view; authenticated users may post and comment.
- Restricted: open view; only members may post or comment.
- Private: fully closed; only members may view, post, or comment.
Whenever an unauthenticated user attempts any membership action, the platform intercepts and presents the authentication modal.
Both writes — the member count adjustment and the membership snippet — are executed in a single Firestore batch write, ensuring they succeed or fail together. Community creation uses a Firestore transaction to prevent duplicate names from being registered concurrently.
flowchart TD
A[User requests content access] --> B{Authenticated?}
B --> |No| C{Privacy type?}
C --> |public| D[Allow view only]
C --> |restricted| E[Allow view only]
C --> |private| F[Deny all — show auth modal]
B --> |Yes| G{Is member?}
G --> |No| H{Privacy type?}
H --> |public| I[Allow view, post, and comment]
H --> |restricted| J[Allow view — block post and comment]
H --> |private| K[Deny all — show restricted banner]
G --> |Yes| L[Allow full access]
J --> M[Prompt user to join]
K --> N[Prompt user to join or request access]
sequenceDiagram
participant User
participant UI
participant Firestore
User->>UI: Clicks Join or Leave
UI->>UI: Check authentication status
alt Not authenticated
UI->>User: Present auth modal
else Authenticated
UI->>Firestore: Open batch write
Firestore->>Firestore: Increment or decrement member count on community document
Firestore->>Firestore: Create or delete membership snippet in user subcollection
Firestore-->>UI: Batch committed
UI->>UI: Update local membership state
UI->>User: Reflect updated membership in UI
end
A user holds admin status if they are either the community's original creator or their user identifier appears in the community document's admin array. The creator's admin status derives from the creator identifier field and cannot be revoked through the admin management panel, which only modifies the admin array. All permission checks are client-side — there are no Firestore Security Rules and no custom Firebase Auth claims. All admin checks are purely client-side UI guards.
The admin management panel is accessible only to existing admins. Admins may search for platform users by email address (exact or partial). The system verifies the account exists before adding. When added, their UID is appended to the admin array and their membership snippet updated. When removed, a confirmation dialog is shown before revocation; no confirmation is shown when granting admin status. After any operation, the Jotai state is patched immediately.
When a user account is deleted, a Cloud Function removes the top-level Firestore user document but does not traverse community documents to remove the UID from admin arrays. Stale identifiers persist indefinitely.
flowchart TD
A[Community created] --> B[Creator automatically holds admin status]
B --> C{Admin action required}
C --> |Grant admin| D[Search for user by email]
D --> E[Verify user account exists]
E --> F[Append UID to admin array on community document]
F --> G[Update user membership snippet to reflect admin status]
G --> H[Patch Jotai state — admin list refreshed in UI]
C --> |Revoke admin| I[Select existing admin from panel]
I --> J[Show confirmation dialog]
J --> |Confirmed| K[Remove UID from admin array]
K --> L[Update membership snippet]
L --> M[Patch Jotai state — admin list refreshed in UI]
J --> |Cancelled| N[No change made]
C --> |Exercise permissions| O[Admin attempts a privileged action]
O --> P{Is creator or present in admin array?}
P --> |Yes| Q[Access granted to admin features]
P --> |No| R[Access denied — action blocked]
B --> S[User account deleted]
S --> T[Top-level user Firestore document removed]
T --> U[UID remains in community admin arrays as orphaned reference]
U --> V[Inert but never cleaned up automatically]
The platform employs a layered guard strategy distributed across three tiers: the rendering layer, the hook layer, and the caller layer.
At the rendering layer, components adapt their presentation: vote buttons are visually disabled with a "not-allowed" cursor; the comment input replaces itself with sign-in prompt buttons when no user is present. Reading posts and comments requires no authentication — content is publicly readable without signing in.
At the hook and caller layers, write actions verify auth before proceeding: creating a post, casting a vote, saving a post, and submitting a comment all open the auth modal when no user is present. For communities with restricted or private settings, creating a post or comment carries a second, independent membership check. Authentication alone is not sufficient — the user must also be a member of the community before posting or commenting.
Deletion actions are UI-gated: the delete affordance is rendered only to the creator or a community admin. The data layer itself has no internal authentication check for deletes.
Vote state is actively cleared when a user signs out, via a synchronisation hook that resets the vote atom on logout. Saved-post identifiers, by contrast, are not cleared on logout and persist in memory until the page is refreshed.
flowchart TD
A([User triggers an action]) --> B{Requires authentication?}
B -- No\nReading posts or comments --> Z([Action permitted — public read])
B -- Yes --> C{Is the user authenticated?}
C -- No --> D{Action type?}
D -- Vote --> E[Vote button visually disabled\nNo modal triggered]
D -- Post / Save / Comment --> F[Authentication modal opened]
D -- Delete --> G[Delete control not rendered\nAction unreachable]
F --> H{Did the user sign in?}
H -- No --> I([Action abandoned])
H -- Yes --> C
C -- Yes --> J{Requires community membership?}
J -- No\nPublic community or non-write action --> Z2([Action executed])
J -- Yes\nRestricted or Private community --> K{Is the user a member?}
K -- Yes --> Z2
K -- No --> L([Action blocked\nRestricted banner shown])
The platform uses three Cloud Functions, each responding to a specific lifecycle event.
This function fires when a new user account is established — through email and password registration, a first OAuth sign-in, or an anonymous sign-in. It does not fire for custom-token-based sign-ins. On invocation, it writes the Firebase User object to a Firestore user document, establishing the canonical user record that the rest of the platform references.
This function fires when a user account is removed individually. It deletes the top-level Firestore user document associated with the removed account. It does not cascade-delete the user's subcollections — community snippets, post votes, and saved posts — which become permanently orphaned. It also does not update community documents to remove the deleted user's UID from admin arrays. A further limitation applies when multiple accounts are deleted simultaneously via the Admin SDK's bulk deletion capability: this trigger does not fire per user in that case, leaving those documents undeleted.
When a post document is deleted, this function queries all users' saved-post references and batch-deletes them. However, Firestore batches are capped at 500 operations; if more than 500 users have saved a post, the batch will fail, leaving stale references.
flowchart LR
subgraph Firebase Authentication
E1([User account created])
E2([User account deleted])
end
subgraph Cloud Firestore
E3([Post document deleted])
end
subgraph Cloud Functions
F1[createUserDocument\nonCreate trigger]
F2[deleteUserDocument\nonDelete trigger]
F3[deleteSavedPost\nonDelete trigger]
end
subgraph Firestore Writes
W1[Create user document\nat users/uid]
W2[Delete top-level user document\nat users/uid]
W3[Batch-delete saved-post references\nacross all users — max 500 ops]
end
E1 -->|fires| F1 --> W1
E2 -->|fires for individual deletion only| F2 --> W2
E3 -->|fires| F3 --> W3
F2 -.->|Does NOT delete| X1([User subcollections\ncommunity snippets, votes, saved posts\npermanently orphaned])
F2 -.->|Does NOT update| X2([Community admin arrays\nstale UID references remain])
E2 -.->|Bulk Admin SDK delete\nDoes NOT fire per user| Y([onDelete not triggered\nfor bulk operations])
W3 -.->|Fails silently if| Z([More than 500 saved references\nbatch ceiling exceeded])
The platform's security model is built entirely on client-side enforcement. There is no server-side middleware — the project contains no middleware that would intercept requests before pages are rendered — and the Firestore database operates without any deployed Security Rules. Firebase Storage likewise has no configured access rules. All access decisions are made by the React component tree and the hooks that supply it with state, using the authentication identity provided by the Firebase client SDK.
In this architecture, the authentication modal, the restricted-community banner, the conditionally rendered delete control, and the disabled vote button are all user-experience constructs. They shape what a legitimate user perceives and can interact with, but they do not constitute a security boundary. A determined actor who bypasses the user interface would encounter no server-side rule to block an unauthorised write.
Firebase's own security model is designed around the opposite assumption. The authoritative enforcement layer in a Firebase-backed application is the combination of Firestore Security Rules evaluated on Google's servers and, for custom back-end logic, server-side ID token verification using the Firebase Admin SDK. Firebase ID tokens are short-lived, RS256-signed JWTs that carry the user's identity and any custom claims, and they can be cryptographically verified without trusting the client. When Security Rules reference the request's authentication context, they are evaluating a server-verified identity. The current codebase does not leverage this layer.
Specific gaps in the current model include the following. The data layer that performs direct Firestore writes has no internal authentication enforcement; it relies entirely on calling components having already verified the user's right to act. The Firestore user document created on account registration contains the full Firebase User object, which may include more information than downstream features require; without Security Rules restricting read access, any authenticated user could potentially read another user's full record. The admin array on community documents retains stale identifiers after account deletions, with no automatic cleanup. Finally, saved-post identifiers are not cleared from client memory when a user signs out — a session-hygiene concern rather than a data-exposure security risk, but one that means a subsequent user on a shared device might briefly observe the previous session's bookmarks before a page reload.
The fundamental distinction is between a user-experience guard and a server-enforced security boundary. UX guards communicate intent and guide legitimate users; they can be circumvented. Security rules enforced on the server cannot be circumvented by client-side manipulation.
flowchart TB
subgraph Client ["Client (Browser) — UX Layer Only"]
UI[React Components\nConditional rendering,\ndisabled controls, modal triggers]
Jotai[Jotai State\nAuth state, vote state,\nsaved posts]
SDK[Firebase Client SDK\nAuth session management,\nFirestore and Storage access]
end
subgraph Firebase ["Firebase Platform"]
Auth[Firebase Authentication\nID token issuance and refresh]
Rules[Firestore Security Rules\nNOT configured in this project]
StorageRules[Storage Security Rules\nNOT configured in this project]
Firestore[(Cloud Firestore)]
Storage[(Firebase Storage)]
Functions[Cloud Functions\nEvent-driven triggers]
end
UI --> Jotai
UI --> SDK
SDK --> Auth
SDK -->|Direct read/write — no rules enforced| Firestore
SDK -->|Direct read/write — no rules enforced| Storage
Auth --> Functions
Firestore --> Functions
Rules -.->|Would enforce server-side\naccess control if configured| Firestore
StorageRules -.->|Would enforce server-side\naccess control if configured| Storage
Rules -.->|Currently absent| X1([Gap: database writes\nnot server-enforced])
StorageRules -.->|Currently absent| X2([Gap: storage access\nnot server-enforced])
style Rules stroke-dasharray: 5 5
style StorageRules stroke-dasharray: 5 5
style X1 stroke-dasharray: 5 5, fill:#fff3cd
style X2 stroke-dasharray: 5 5, fill:#fff3cd
-
Google. "Get Started with Firebase Authentication on Websites." Firebase Documentation.
-
Google. "Authenticate Using Google with JavaScript." Firebase Documentation.
-
Google. "Authenticate Using GitHub with JavaScript." Firebase Documentation.
-
Google. "Firebase Authentication triggers." Firebase Documentation.
-
Google. "Structuring Cloud Firestore Security Rules." Firebase Documentation.
-
Google. "Get started with Cloud Firestore Security Rules." Firebase Documentation.
-
CSFrequency. "React Firebase Hooks." GitHub. https://github.com/CSFrequency/react-firebase-hooks. (Accessed 2026.)