Authentication is a crucial part of many web applications, ensuring that users have secure access to their accounts and data. Firebase, a cloud-based platform by Google, provides a convenient way to implement authentication in your React application. In this article, we'll guide you through building a basic authentication system using Firebase and React, while also emphasizing the importance of keeping your Firebase API keys secure.
Prerequisites
Before diving into the code, make sure you have the following set up:
Node.js and npm (Node Package Manager) installed on your system.
A Firebase project set up on the Firebase Console.
Firebase CLI installed (
npm install -g firebase-tools
).
Setting Up Firebase
To securely access your Firebase project in your React app, you should not hardcode your Firebase API keys directly in your code, as they could be exposed in public repositories. Instead, you should store them in environment variables. Here's how you can do it:
In your Firebase project settings, go to the "General" tab.
Scroll down to the "Your apps" section and select the Firebase web app you've created.
Under "Firebase SDK snippet," select the "Config" option. This will reveal your Firebase configuration object.
Store the Firebase configuration object in an environment variable. You can use a
.env
file to manage your environment variables. Create a.env
file in your project's root directory and add the following:
REACT_APP_FIREBASE_API_KEY=YOUR_API_KEY
REACT_APP_FIREBASE_AUTH_DOMAIN=YOUR_AUTH_DOMAIN
REACT_APP_FIREBASE_PROJECT_ID=YOUR_PROJECT_ID
REACT_APP_FIREBASE_STORAGE_BUCKET=YOUR_STORAGE_BUCKET
REACT_APP_FIREBASE_MESSAGING_SENDER_ID=YOUR_MESSAGING_SENDER_ID
REACT_APP_FIREBASE_APP_ID=YOUR_APP_ID
Replace YOUR_API_KEY
, YOUR_AUTH_DOMAIN
, etc., with the values from your Firebase configuration object.
Installing Dependencies
To get started with React and Firebase authentication, install the necessary dependencies:
npm install react react-dom react-router-dom firebase
Creating the Firebase Configuration File
Now, let's create a Firebase configuration file to initialize Firebase in your React app. Create a file named firebaseConfig.js
in your project's root directory with the following content:
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID,
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
export default auth;
This file initializes Firebase with the configuration stored in environment variables and exports the auth
object for use in authentication-related components.
Building Authentication Components
Now, let's create the authentication components for login and sign-up.
Login Component
import { useRef, useState } from "react";
import { Link } from "react-router-dom";
import {
sendPasswordResetEmail,
signInWithEmailAndPassword,
} from "firebase/auth";
import auth from "../../firebase/firebaseConfig";
const Login = () => {
const [loginError, setLoginError] = useState("");
const [success, setSuccess] = useState("");
const emailRef = useRef(null);
const handleLogin = (e) => {
e.preventDefault();
const email = e.target.email.value;
const password = e.target.password.value;
setLoginError("");
setSuccess("");
// Attempt to sign in with email and password
signInWithEmailAndPassword(auth, email, password)
.then((result) => {
// Check if the user's email is verified
if (result.user.emailVerified) {
setSuccess("Logged in Successfully");
} else {
alert("Please verify your email");
}
})
.catch((error) => {
setLoginError(error.message);
});
};
const handleForgetPassword = () => {
const email = emailRef.currentUser.value;
if (!email) {
console.log("Please provide an email", emailRef.current.value);
return;
} else if (
!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email)
) {
console.log("Please write a valid email");
return;
}
// Send a password reset email
sendPasswordResetEmail(auth, email)
.then(() => {
alert("Please check your email for password reset instructions");
})
.catch((error) => {
console.log(error);
});
};
return (
<div>
{/* Login form */}
<div className="hero min-h-screen bg-base-200">
<div className="hero-content flex-col lg:flex-row-reverse">
<div className="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
<div className="card-body">
<form onSubmit={handleLogin}>
{/* Email input */}
<div className="form-control">
<label className="label">
<span className="label-text">Email</span>
</label>
<input
type="text"
name="email"
placeholder="Email"
ref={emailRef}
className="input input-bordered"
/>
</div>
{/* Password input */}
<div className="form-control">
<label className="label">
<span className="label-text">Password</span>
</label>
<input
type="password"
name="password"
placeholder="Password"
className="input input-bordered"
/>
<label className="label">
<a
onClick={handleForgetPassword}
href="#"
className="label-text-alt link link-hover"
>
Forgot password?
</a>
</label>
</div>
{/* Submit button */}
<div className="form-control mt-6">
<button className="btn btn-primary">Login</button>
</div>
</form>
{/* Display login error and success messages */}
{loginError && <p className="text-red-700">{loginError}</p>}
{success && <p className="text-green-600">{success}</p>}
{/* Link to Sign-Up */}
<p>
New to this website? Please <Link to="/sign-up">Register</Link>
</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default Login;
Here's an explanation of the key parts of the Login component:
useState: We use the
useState
hook to manage state forloginError
(to store login errors) andsuccess
(to show a success message).emailRef: This is a
useRef
that allows us to access the email input field later.handleLogin: This function is called when the login form is submitted. It prevents the default form submission, retrieves the email and password from the form inputs, and attempts to sign in using Firebase's
signInWithEmailAndPassword
method. If successful, it checks if the user's email is verified. If not, it displays a message to verify the email. If there's an error, it sets theloginError
state with the error message.handleForgetPassword: This function is called when the "Forgot password?" link is clicked. It retrieves the email from the input field, validates it, and sends a password reset email using Firebase's
sendPasswordResetEmail
method.Form Markup: The login form is defined with email and password inputs, and a "Forgot password?" link. The form submission triggers the
handleLogin
function.Display Error and Success Messages: We conditionally display error and success messages based on the state variables.
Link to Sign-Up: We provide a link to the Sign-Up page for new users.
Sign-Up Component
import { useState } from "react";
import { Link } from "react-router-dom";
import {
createUserWithEmailAndPassword,
sendEmailVerification,
updateProfile,
} from "firebase/auth";
import auth from "../../firebase/firebaseConfig";
import { FaEye, FaEyeSlash } from "react-icons/fa";
const Sign_Up = () => {
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [showPassword, setShowPassword] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
const email = e.target.email.value;
const password = e.target.password.value;
const accepted = e.target.terms.checked;
const name = e.target.name.value;
setError("");
// Validate password
if (password.length < 6) {
setError("Password must be at least 6 characters");
return;
} else if (!/[A-Z]/.test(password)) {
setError("Your password should have at least one uppercase character.");
return;
} else if (!accepted) {
setError("Please accept our terms and conditions!");
return;
}
// Create a new user account with Firebase
createUserWithEmailAndPassword(auth, email, password)
.then((result) => {
setSuccess("User Created Successfully.");
// Update user profile with a display name
updateProfile(result.user, {
displayName: name,
});
// Send email verification
sendEmailVerification(result.user).then(() => {
alert("Please check your email and verify your account");
});
})
.catch((error) => setError(error.message));
};
return (
<div className="">
<div className="mx-auto md:w-1/2">
<h2 className="text-3xl mb-8">Please Register</h2>
<form onSubmit={handleSubmit}>
{/* Name input */}
<input
className="mb-4 w-full py-
2 px-4"
type="text"
name="name"
placeholder="Your Name"
required
/>
<br />
{/* Email input */}
<input
className="mb-4 w-full py-2 px-4"
type="email"
name="email"
placeholder="Email Address"
required
/>
<br />
{/* Password input */}
<div className="mb-4 relative border">
<input
className="w-full py-2 px-4"
type={showPassword ? "text" : "password"}
name="password"
placeholder="Password"
required
/>
<span
className="absolute top-3 right-2"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <FaEyeSlash></FaEyeSlash> : <FaEye></FaEye>}
</span>
</div>
<br />
{/* Terms and Conditions checkbox */}
<div className="mb-2">
<input type="checkbox" name="terms" id="terms" required />
<label className="ml-2" htmlFor="terms">
Accept our <a>Terms and Conditions</a>
</label>
</div>
<br />
{/* Submit button */}
<input
className="btn btn-secondary mb-4 w-full"
type="submit"
value="Register"
/>
</form>
{/* Display error and success messages */}
{error && <p className="text-red-700">{error}</p>}
{success && <p className="text-green-600">{success}</p>}
{/* Link to Login */}
<p>
Already have an account? Please <Link to="/login">Login</Link>
</p>
</div>
</div>
);
};
export default Sign_Up;
Here's an explanation of the key parts of the Sign-Up component:
useState: We use the
useState
hook to manage state forerror
(to store sign-up errors) andsuccess
(to show a success message).showPassword: This state variable is used to toggle the visibility of the password input.
handleSubmit: This function is called when the sign-up form is submitted. It prevents the default form submission, retrieves user information (name, email, password, and terms acceptance), and validates the password. If everything is valid, it creates a new user account using Firebase's
createUserWithEmailAndPassword
method, updates the user's display name, and sends an email verification request usingsendEmailVerification
. If there's an error, it sets theerror
state with the error message.Form Markup: The sign-up form is defined with inputs for name, email, password, and a checkbox to accept terms and conditions. The form submission triggers the
handleSubmit
function.Display Error and Success Messages: We conditionally display error and success messages based on the state variables.
Link to Login: We provide a link to the Login page for users who already have an account.
These components handle user authentication, validation, and error handling for login and sign-up functionalities in your React application using Firebase Authentication.
Setting Up Routing
Now, let's set up routing for our authentication components.
// router.js
import { createBrowserRouter } from "react-router-dom";
import App from "../App";
import Login from "../components/Login/Login";
import Sign_Up from "../components/Sign-Up/Sign-Up";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
path: "/login",
element: <Login />,
},
{
path: "/sign-up",
element: <Sign_Up />,
},
],
},
]);
export default router;
Integrating Authentication into Your App
To integrate authentication into your app, import the router
component into your app's entry point (e.g., index.js
) and render it using React's ReactDOM
.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { RouterProvider } from "react-router-dom";
import router from "./Route/Route";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
Protecting Firebase API Keys
Remember that you should never expose your Firebase API keys in public repositories or client-side code. By using environment variables as demonstrated in the firebaseConfig.js
file, you can keep your keys safe and separate from your codebase.
Conclusion
In this article, we've covered the basic setup of Firebase authentication in a React application. We've created login and sign-up components, set up routing, and emphasized the importance of keeping your Firebase API keys secure using environment variables. With this foundation, you can build more advanced authentication features and enhance the security of your React application.