Authentication with Firebase
Firebase provides a robust and secure authentication system that integrates seamlessly into your application. This boilerplate leverages Firebase Authentication to handle user login, registration, and session management with ease.
By using Firebase Authentication, you can significantly reduce the amount of code required to implement and maintain a secure authentication system. Firebase offers a pre-built, scalable solution that accelerates development time, allowing you to focus on building core features for your application. Additionally, it provides out-of-the-box support for various authentication methods, robust session management, and enhanced security practices, making it an ideal choice for modern applications.
Key Features
1. Sign In with Email and Password
This boilerplate implements Firebase Authentication using a custom wrapper function called signIn
, which abstracts Firebase’s signInWithEmailAndPassword
function. This abstraction streamlines the authentication process by adding extra checks, such as verifying the user's email, and improves error handling to ensure seamless integration and enhanced security in your application.
The signIn
Function
The signIn
function is a custom wrapper around Firebase's signInWithEmailAndPassword
method. It abstracts the authentication process by adding extra checks and error handling to ensure that only verified users can proceed. This provides a seamless integration with the application and enhances security.
Here’s the implementation of the signIn
function:
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
export const signIn = async (email, password) => {
try {
const credential = await signInWithEmailAndPassword(auth, email, password);
if (!credential?.user || !credential.user?.emailVerified) {
return false; // Return false if email is not verified
}
return credential; // Return the credential if the email is verified
} catch (error) {
throw error; // Throw the error to be handled by the calling function
}
};
File Path: src/services/user.js
The function:
- Authenticates the user with their email and password.
- Checks that the user's email is verified before allowing access.
- Throws an error if something goes wrong, allowing the calling function to handle the error gracefully.
Allowing Unverified Emails
By default, the signIn
function checks whether the user's email is verified before granting access. However, in scenarios where email verification is not required, the function can be modified to bypass this check. This adjustment allows users with unverified email addresses to sign in successfully.
Here’s the modified signIn
function:
export const signIn = async (email, password) => {
try {
const credential = await signInWithEmailAndPassword(auth, email, password);
return credential; // Allow users to sign in without checking email verification
} catch (error) {
throw error;
}
};
In addition to modifying the signIn
function, the email verification check should also be removed from the AuthStateVerifier
utility component, located in components/common/AuthStateVerifier/index.js
.
Currently, the component checks if the user's email is verified using user?.emailVerified
. This logic ensures only verified users can access the application. To allow unverified users, this check needs to be removed.
Here’s the original implementation:
const unsubscribe = auth.onAuthStateChanged(user => {
if (!user?.emailVerified) {
signOut(auth);
setTimeout(() => router.push("/login"), 550);
onComplete?.();
return;
}
onAuthStateChangeSuccess?.();
setAuthUser(user);
loadUserProfileById(user.uid);
if (redirectPath) {
router.push(redirectPath);
}
onComplete?.();
});
File Path: src/components/common/AuthStateVerifier/index.js
To disable email verification, you should remove the user?.emailVerified
condition and adjust the logic accordingly:
const unsubscribe = auth.onAuthStateChanged(user => {
if (!user) {
signOut(auth);
setTimeout(() => router.push("/login"), 550);
onComplete?.();
return;
}
onAuthStateChangeSuccess?.();
setAuthUser(user);
loadUserProfileById(user.uid);
if (redirectPath) {
router.push(redirectPath);
}
onComplete?.();
});
This modification allows users to proceed regardless of their email verification status, ensuring that the entire authentication flow aligns with the new policy.
Usage in the Login Page
The signIn
function is the main function used in the Login Page for authenticating users via email and password. When the user submits the login form, the handleSubmit
function is triggered, calling the signIn
function with the user's email and password. This ensures that the user is authenticated and only verified users are granted access.
Here’s how the signIn
function is used in the Login Page:
const handleSubmit = async e => {
e.preventDefault();
setIsLoading(true);
try {
const credential = await signIn(email, password);
if (!credential) {
showNotification(
"Your email address has not been verified yet. Please check your email for the verification link.",
"error"
);
await logOut();
return;
}
const user = credential?.user;
onSignInSuccess?.(user);
} catch (err) {
showNotification(translateError(err.message), "error");
} finally {
setIsLoading(false);
}
};
File Path: src/components/forms/LoginForm/index.js
2. Social Logins (Sign Up & Sign In)
Enhance user experience by simplifying the login process with social media integrations, allowing users to authenticate via popular third-party providers like Google, Facebook, and more. Firebase makes it effortless to configure and manage these providers. In ShipFast, Google authentication is already integrated, and all that’s needed is to ensure that the Google Provider is enabled in your Firebase Authentication settings. You can also enable additional platforms, such as Facebook, by configuring the respective provider in Firebase, which will be seamlessly integrated into the system.
The signInUserWithGoogleProvider
Function
The signInUserWithGoogleProvider
function serves as a wrapper around the registerUserWithGoogleProvider
function, which leverages Firebase's signInWithPopup
method for Google authentication. This function ensures a smooth process by not only authenticating the user but also verifying if they are new or returning. Once authentication is successful, the user is automatically registered within the application, and an organization is created on their behalf, streamlining the sign-up process.
The user’s data is stored in the Firestore users
collection, where their details are saved for easy retrieval. Additionally, the new user will receive a Free subscription by default, which is registered via Stripe. This subscription is stored in the subscriptions
subcollection within the respective organization’s document (i.e., organizations/{orgId}/subscriptions
). The subscription data is sent to Firestore through a Stripe webhook event once the user successfully registers.
For more details about the subscription data synchronization, see Payment Feature Documentation.
By using this method, users enjoy a frictionless experience, logging in or signing up without additional manual steps, while Firebase ensures that user authentication and registration are handled efficiently.
Here’s the implementation of the registerUserWithGoogleProvider
function:
import { auth, signInWithPopup, googleProvider } from "@/config/firebase";
export const registerUserWithGoogleProvider = () => {
return new Promise(async (resolve, reject) => {
try {
// Trigger Google sign-in popup using Firebase Authentication
const { user } = await signInWithPopup(auth, googleProvider);
// Check if user is null or undefined, reject if sign-in fails
if (!user) {
return reject(new Error("User registration failed."));
}
const userId = user.uid;
// Check if the user already exists in the database
const existingUser = await getUserById(userId);
if (existingUser) {
return resolve({ message: "User already exists.", user: existingUser });
}
// Create a default organization for the user
const orgName = `${user.email || user.displayName} Org`;
// Simultaneously create organization and user records in the database
const [organization] = await Promise.all([
createOrganizationAndAttachUser(user.uid, {
name: orgName,
billingEmail: user.email || ""
}),
addUser({
userId,
email: user.email || "",
name: user.displayName || ""
})
]);
// Retrieve the organization ID
const orgId = organization?.id || organization?.organizationId;
// Create metadata to be used for subscription registration
const metadata = {
organizationId: orgId,
userId: user.uid,
email: user.email
};
// Register the user for a free subscription plan in Stripe
await createStripeFreeSubscription(user.email, orgId, metadata);
// Resolve the promise indicating successful registration
resolve({ user, organization });
} catch (error) {
// Reject the promise with the error if any step fails
reject(error);
}
});
};
The function:
- Initiates the Google authentication popup through Firebase's
signInWithPopup
. - Checks if the user exists in the system to avoid duplicates.
- Creates a new organization for the user and adds them to the database.
- Sends a welcome notification email to the user.
- Registers the user for a free subscription in Stripe, and subscription details are stored in the
subscriptions
subcollection oforganizations/{orgId}/subscriptions
.
Usage in the Login Page
The signInUserWithGoogleProvider
function is used in the Login Page to authenticate users via Google. Upon clicking the "Login with Google" button, the handleGoogleLogin
function is triggered, which calls signInUserWithGoogleProvider
. This ensures that the user is authenticated and properly registered in the system.
Here’s how the signInUserWithGoogleProvider
function is used in the Login Page:
import { signInUserWithGoogleProvider } from "@/services/user";
const handleGoogleLogin = async () => {
setIsLoading(true);
try {
const { user, organization } = await signInUserWithGoogleProvider();
if (!user) {
showNotification("Google login failed. Please try again.", "error");
return;
}
// Proceed with any post-login actions
onSignInSuccess?.(user);
} catch (err) {
showNotification(translateError(err.message), "error");
} finally {
setIsLoading(false);
}
};
File Path: src/components/forms/LoginForm/index.js
In this code:
- The
handleGoogleLogin
function is triggered when the user clicks theSign in with Google
button. - It calls the
signInUserWithGoogleProvider
function to handle the authentication and registration. - If successful, the user is logged in and can proceed to the next steps in the application.
- Any errors are caught and displayed as notifications.
This approach ensures a smooth authentication process for users, while also integrating essential backend logic for user management and subscription registration.
3. Register using Email and Password
The registerUser function allows users to create a new account using their email and password, while performing the following operations:
-
Firebase Authentication: Registers the user using Firebase's built-in
createUserWithEmailAndPassword
method. This method securely stores the user's email and password in Firebase Authentication, a service that handles authentication and user management. By using this service, the user's credentials are stored securely, and the system ensures that only valid users can access their accounts. -
Organization Creation and Attach User: Creates a new organization and attaches the user to it using the
createOrganizationAndAttachUser
function. The organization is stored in theorganizations
collection, while the organization ID and the user ID from Firebase Authentication are stored in theuser_organizations
collection. For details of the fields and structure, see the Database Diagram. -
User Record Creation: Adds the user to the Firestore
users
collection using theaddUser
function. This ensures that the user's details are securely stored and can be easily retrieved for future reference. For more information on the fields and structure of the user record, see the Database Diagram. -
Email Verification: Sends a verification email to the user through Firebase's
sendEmailVerification
method. This step ensures that the email address provided by the user is valid and that they have access to it. The user must click the verification link in the email to confirm their account, helping to prevent fraud and ensuring the accuracy of the user’s contact details. -
Stripe Subscription: Creates a free subscription on Stripe for the new user's organization. Upon successful creation of the subscription, Stripe triggers an API call to the designated ShipFast API route,
/api/stripe/events
, which handles syncing the subscription data. (For more details about this API route, see /Stripe Event Setup.) This data, which includes a summary of the subscription, is then stored in ShipFast's Firestore under thesubscriptions
subcollection within the respective organization's document (i.e.,organizations/{orgId}/subscriptions
).
Here’s the function code:
export const registerUser = (email, password) => {
return new Promise(async (resolve, reject) => {
try {
// Create a Firebase Auth account using the provided email and password
const { user } = await createUserWithEmailAndPassword(
auth,
email,
password
);
if (!user) {
return reject(new Error("User creation failed")); // Reject if user creation is unsuccessful
}
const orgName = `${user.email || user.displayName} Org`; // Default organization name based on user info
// Execute multiple asynchronous operations concurrently
const [organization] = await Promise.all([
// Create an organization in the database linked to the user
createOrganizationAndAttachUser(user.uid, {
name: orgName,
billingEmail: user.email || ""
}),
// Create a user record in the database
addUser({
userId: user.uid,
email: user.email || "",
name: user.displayName || ""
}),
// Send an email verification to the user
sendEmailVerification(user)
]);
const orgId = organization?.id || organization?.organizationId; // Retrieve the organization ID
const metadata = {
organizationId: orgId,
userId: user.uid,
email: user.email
};
await createStripeFreeSubscription(user.email, orgId, metadata);
resolve(); // Resolve the promise upon successful registration
} catch (error) {
reject(new Error(`Registration failed: ${error.message}`)); // Reject the promise with an error message
}
});
};
4. Change Password
ShipFast provides a built-in feature to allow users to update their passwords easily. This utilizes Firebase's updatePassword method, which securely handles password changes for Firebase Authentication accounts. The following operations are performed:
-
Firebase Authentication: Uses Firebase's built-in
updatePassword
method to update the user's password. This method securely updates the password associated with the user's Firebase Authentication account, ensuring that only authenticated users can modify their password. -
Password Security: Ensures that the new password meets security standards, such as length and complexity, to prevent weak passwords from being set. Firebase handles all necessary validation for security.
Here’s how it is used in the component:
const handleSave = async e => {
e.preventDefault();
const user = authUser; // Get the authenticated user
// Validate password fields
const validationError = validatePasswords(password, confirmPassword);
if (validationError) {
showNotification(validationError, "error");
return;
}
try {
setIsSaving(true);
await updatePassword(user, password);
showNotification(
"Password updated successfully! Your changes have been saved."
);
resetPasswordFields();
} catch (err) {
setIsReAuthOpen(
Boolean(err?.message?.includes("auth/requires-recent-login"))
); // Opens a modal for re-login if the user hasn't logged in recently
} finally {
setIsSaving(false);
}
};
File Path: src/components/forms/ChangePasswordForm/index.js
In this implementation:
- The
handleSave
function validates the password fields and ensures that the passwords match. - The
updatePassword
function is called to update the password for the authenticated user. - If the update is successful, a success notification is displayed and the password fields are reset.
- If the user is required to re-authenticate, the
setIsReAuthOpen
state will open a modal for the user to log in again. The app requires a re-login for users who haven't logged in recently.
Important Notes
-
Authorized Domains: To ensure proper functioning of Firebase Authentication, you must add the domain you are using to the Authorized Domains section in your Firebase Authentication settings. This is necessary to allow Firebase to handle authentication requests from your application.
-
Sign-In Providers: Make sure to enable and configure the Sign-In Providers you want to use in your Firebase Authentication settings. These include options like Email/Password, Google, Facebook, or other supported providers. Proper configuration ensures a seamless authentication experience for users.