SimpleNext.js
How to use Firebase authentification with Next.js
Authentification is one of the most used features of any web app, that developers had for years to implement by themselves. Luckily in the last years many services offer a simpler implementation of authentification (oAuth, passport.js, supabase, …) , and firebase is certainly on of the most used and most simple to implement.
What is firebase
Firebase is a Google platform that offers a suite of products aimed to simplify web development , such as :
- A realtime database (named firestore)
- Authentification
- Site hosting
- Storage
- Notifications and cloud messaging
- ML
- ….
Create a firebase account
You need first to create your firebase account here . Then you have to create a project.
Now, click on the settings icon right beside Project Overview (in the top left part of your screen).
Under General, you can see your app and the configuration.
Now you need to choose the sign-in method to use. You can use google authentification, facebook authentification, ... We will be focusing on the usual email/password method.
Installing Firebase
Let's create a new Next.js app
npx create-next-app firebase-app
then install firebase :
npm install --save Firebase
# or
yarn add Firebase
Add Firebase Keys to the project
for the local testing, you can add the keys to your .env.local file :
NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY=<YOUR_API_KEY>
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=<YOUR_DOMAIN>
NEXT_PUBLIC_FIREBASE_PROJECT_ID=<YOUR_PROJECT_ID>
P.S : don’t forget to add the .env.local file to your .gitignore if you commit to Github
For production environment, you can use vercel secrets if you deploy to vercel, or any equivalent depending on the platform you deploy on.
Create a clientApp.js file in the src/lib directory (create lib directory if not created already) :
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
const FirebaseCredentials = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID
}
// if a Firebase instance doesn't exist, create one
if (!firebase.apps.length) {
firebase.initializeApp(FirebaseCredentials)
}
export default firebase;
Create an useFirebaseAuth.js file in lib directory :
import { useState, useEffect } from 'react'
import Firebase from './Firebase';
const formatAuthUser = (user) => ({
uid: user.uid,
email: user.email
});
export default function useFirebaseAuth() {
const [authUser, setAuthUser] = useState(null);
const [loading, setLoading] = useState(true);
const authStateChanged = async (authState) => {
if (!authState) {
setAuthUser(null)
setLoading(false)
return;
}
setLoading(true)
var formattedUser = formatAuthUser(authState);
setAuthUser(formattedUser);
setLoading(false);
};
// listen for Firebase state change
useEffect(() => {
const unsubscribe = Firebase.auth().onAuthStateChanged(authStateChanged);
return () => unsubscribe();
}, []);
return {
authUser,
loading
};
}
Create a context directory under src then create the AuthUserContext.js file under it :
import { createContext, useContext, Context } from 'react'
import useFirebaseAuth from '../lib/useFirebaseAuth';
const authUserContext = createContext({
authUser: null,
loading: true
});
export function AuthUserProvider({ children }) {
const auth = useFirebaseAuth();
return <authUserContext.Provider value={auth}>{children}</authUserContext.Provider>;
}
// custom hook to use the authUserContext and access authUser and loading
export const useAuth = () => useContext(authUserContext);
then in _app.js add the user context created :
import { AuthUserProvider } from '../context/AuthUserContext';
function MyApp({ Component, pageProps }) {
return <AuthUserProvider><Component {...pageProps} /></AuthUserProvider>
}
export default MyApp
Create protected routes in your app
This will be the page that you will be redirected to if the signup is successful :
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '../context/AuthUserContext';
import {Container, Row, Col} from 'reactstrap';
const LoggedIn = () => {
const { authUser, loading } = useAuth();
const router = useRouter();
// Listen for changes on loading and authUser, redirect if needed
useEffect(() => {
if (!loading && !authUser)
router.push('/')
}, [authUser, loading])
return (
//Your logged in page
)
}
export default LoggedIn;
Adding login, sign-up, and sign-out functionalities in Next.js
To sign in, sign out or sign up to your app, firebase authentification provides out-of-the box methods:
- signInWithEmailAndPassword : for signing in with email and password.
- createUserWithEmailAndPassword : for signing up with email and password.
- signOut : for signing out.
Add these functions given by firebase to useFirebaseAuth.js under lib:
export default function useFirebaseAuth() {
// ...
const clear = () => {
setAuthUser(null);
setLoading(true);
};
const signInWithEmailAndPassword = (email, password) =>
Firebase.auth().signInWithEmailAndPassword(email, password);
const createUserWithEmailAndPassword = (email, password) =>
Firebase.auth().createUserWithEmailAndPassword(email, password);
const signOut = () =>
Firebase.auth().signOut().then(clear);
useEffect(() => {
const unsubscribe = Firebase.auth().onAuthStateChanged(authStateChanged);
return () => unsubscribe();
}, []);
return {
authUser,
loading,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut
};
}
Don’t forget to update your default value with these functions in your context file.
const authUserContext = createContext({
authUser: null,
loading: true,
signInWithEmailAndPassword: async () => {},
createUserWithEmailAndPassword: async () => {},
signOut: async () => {}
});
export function AuthUserProvider({ children }) {
const auth = useFirebaseAuth();
return <authUserContext.Provider value={auth}>{children}</authUserContext.Provider>;
}
Creating the sign-up page
In your sign-up page, use your useAuth hook to retrieve your function for creating a user once again. createUserWithEmailAndPassword takes two parameters: email and password. After finishing form validation, call this function. If it returns successfully with an authUser, then you can redirect the user accordingly.
import { useState } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '../context/AuthUserContext';
import {Container, Row, Col, Button, Form, FormGroup, Label, Input, Alert} from 'reactstrap';
const SignUp = () => {
const [email, setEmail] = useState("");
const [passwordOne, setPasswordOne] = useState("");
const [passwordTwo, setPasswordTwo] = useState("");
const router = useRouter();
const [error, setError] = useState(null);
const { createUserWithEmailAndPassword } = useAuth();
const onSubmit = event => {
setError(null)
//check if passwords match. If they do, create user in Firebase
// and redirect to your logged in page.
if(passwordOne === passwordTwo)
createUserWithEmailAndPassword(email, passwordOne)
.then(authUser => {
console.log("Success. The user is created in Firebase")
router.push("/logged_in");
})
.catch(error => {
// An error occurred. Set error message to be displayed to user
setError(error.message)
});
else
setError("Password do not match")
event.preventDefault();
};
return (
<Container className="text-center custom-container">
<Row>
<Col>
<Form
className="custom-form"
onSubmit={onSubmit}>
{ error && <Alert color="danger">{error}</Alert>}
<FormGroup row>
<Label for="signUpEmail" sm={4}>Email</Label>
<Col sm={8}>
<Input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
name="email"
id="signUpEmail"
placeholder="Email" />
</Col>
</FormGroup>
<FormGroup row>
<Label for="signUpPassword" sm={4}>Password</Label>
<Col sm={8}>
<Input
type="password"
name="passwordOne"
value={passwordOne}
onChange={(event) => setPasswordOne(event.target.value)}
id="signUpPassword"
placeholder="Password" />
</Col>
</FormGroup>
<FormGroup row>
<Label for="signUpPassword2" sm={4}>Confirm Password</Label>
<Col sm={8}>
<Input
type="password"
name="password"
value={passwordTwo}
onChange={(event) => setPasswordTwo(event.target.value)}
id="signUpPassword2"
placeholder="Password" />
</Col>
</FormGroup>
<FormGroup row>
<Col>
<Button>Sign Up</Button>
</Col>
</FormGroup>
</Form>
</Col>
</Row>
</Container>
)
}
export default SignUp;
Adding a sign-out button
Signing out is also very straightforward. Grab the signOut() function from useAuth() and add it to a button or a link.
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '../context/AuthUserContext';
import {Container, Row, Col, Button} from 'reactstrap';
const LoggedIn = () => {
const { authUser, loading, signOut } = useAuth();
const router = useRouter();
// Listen for changes on loading and authUser, redirect if needed
useEffect(() => {
if (!loading && !authUser)
router.push('/')
}, [authUser, loading])
return (
<Container>
// ...
<Button onClick={signOut}>Sign out</Button>
// ...
</Container>
)
}
export default LoggedIn;
Creating a login page
And finally, the login functionality! It’s exactly the same as the previous two. Retrieve signInWithEmailAndPassword() from useAuth() and pass in the user’s email and password. If they are correct, redirect the user, and, if not, display the correct error message.
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useAuth } from '../context/AuthUserContext';
import {Container, Row, Col, Button, Form, FormGroup, Label, Input, Alert} from 'reactstrap';
export default function Home() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const router = useRouter();
const { signInWithEmailAndPassword } = useAuth();
const onSubmit = event => {
setError(null)
signInWithEmailAndPassword(email, password)
.then(authUser => {
router.push('/logged_in');
})
.catch(error => {
setError(error.message)
});
event.preventDefault();
};
return (
<Container className="text-center" style={{ padding: '40px 0px'}}>
<Row>
<Col>
<h2>Login</h2>
</Col>
</Row>
<Row style={{maxWidth: '400px', margin: 'auto'}}>
<Col>
<Form onSubmit={onSubmit}>
{ error && <Alert color="danger">{error}</Alert>}
<FormGroup row>
<Label for="loginEmail" sm={4}>Email</Label>
<Col sm={8}>
<Input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
name="email"
id="loginEmail"
placeholder="Email" />
</Col>
</FormGroup>
<FormGroup row>
<Label for="loginPassword" sm={4}>Password</Label>
<Col sm={8}>
<Input
type="password"
name="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
id="loginPassword"
placeholder="Password" />
</Col>
</FormGroup>
<FormGroup row>
<Col>
<Button>Login</Button>
</Col>
</FormGroup>
<FormGroup row>
<Col>
No account? <Link href="/sign_up">Create one</Link>
</Col>
</FormGroup>
</Form>
</Col>
</Row>
</Container>
)
}
Final result
Our login page is index.js:
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useAuth } from '../context/AuthUserContext';
import {Container, Row, Col, Button, Form, FormGroup, Label, Input, Alert} from 'reactstrap';
export default function Home() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const router = useRouter();
const { signInWithEmailAndPassword } = useAuth();
const onSubmit = event => {
setError(null)
signInWithEmailAndPassword(email, password)
.then(authUser => {
console.log('tessst')
router.push('/logged_in');
})
.catch(error => {
setError(error.message)
});
event.preventDefault();
};
return (
<Container className="text-center" style={{ padding: '40px 0px'}}>
<Row>
<Col>
<h2>Login</h2>
</Col>
</Row>
<Row style={{maxWidth: '400px', margin: 'auto'}}>
<Col>
<Form onSubmit={onSubmit}>
{ error && <Alert color="danger">{error}</Alert>}
<FormGroup row>
<Label for="loginEmail" sm={4}>Email</Label>
<Col sm={8}>
<Input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
name="email"
id="loginEmail"
placeholder="Email" />
</Col>
</FormGroup>
<FormGroup row>
<Label for="loginPassword" sm={4}>Password</Label>
<Col sm={8}>
<Input
type="password"
name="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
id="loginPassword"
placeholder="Password" />
</Col>
</FormGroup>
<FormGroup row>
<Col>
<Button>Login</Button>
</Col>
</FormGroup>
<FormGroup row>
<Col>
No account? <Link href="/sign_up">Create one</Link>
</Col>
</FormGroup>
</Form>
</Col>
</Row>
</Container>
)
}
if the login is unsuccessful :
our sign up page is sign_up.js :
import { useState } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '../context/AuthUserContext';
import {Container, Row, Col, Button, Form, FormGroup, Label, Input, Alert} from 'reactstrap';
const SignUp = () => {
const [email, setEmail] = useState("");
const [passwordOne, setPasswordOne] = useState("");
const [passwordTwo, setPasswordTwo] = useState("");
const router = useRouter();
const [error, setError] = useState(null);
const { createUserWithEmailAndPassword } = useAuth();
const onSubmit = event => {
setError(null)
//check if passwords match. If they do, create user in Firebase
// and redirect to your logged in page.
if(passwordOne === passwordTwo)
createUserWithEmailAndPassword(email, passwordOne)
.then(User => {
console.log("Success. The user is created in Firebase")
router.push("/logged_in");
})
.catch(error => {
// An error occurred. Set error message to be displayed to user
setError(error.message)
});
else
setError("Password do not match")
event.preventDefault();
};
return (
<Container className="text-center custom-container">
<Row>
<Col>
<Form
className="custom-form"
onSubmit={onSubmit}>
{ error && <Alert color="danger">{error}</Alert>}
<FormGroup row>
<Label for="signUpEmail" sm={4}>Email</Label>
<Col sm={8}>
<Input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
name="email"
id="signUpEmail"
placeholder="Email" />
</Col>
</FormGroup>
<FormGroup row>
<Label for="signUpPassword" sm={4}>Password</Label>
<Col sm={8}>
<Input
type="password"
name="passwordOne"
value={passwordOne}
onChange={(event) => setPasswordOne(event.target.value)}
id="signUpPassword"
placeholder="Password" />
</Col>
</FormGroup>
<FormGroup row>
<Label for="signUpPassword2" sm={4}>Confirm Password</Label>
<Col sm={8}>
<Input
type="password"
name="password"
value={passwordTwo}
onChange={(event) => setPasswordTwo(event.target.value)}
id="signUpPassword2"
placeholder="Password" />
</Col>
</FormGroup>
<FormGroup row>
<Col>
<Button>Sign Up</Button>
</Col>
</FormGroup>
</Form>
</Col>
</Row>
</Container>
)
}
export default SignUp;
if the Sign In is successful, we are redirected to the logged_in.js page :
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '../context/AuthUserContext';
import {Container, Row, Col, Button} from 'reactstrap';
const LoggedIn = () => {
const { authUser, loading, signOut } = useAuth();
const router = useRouter();
// Listen for changes on loading and authUser, redirect if needed
useEffect(() => {
if (!loading && !authUser)
router.push('/')
}, [authUser, loading])
return (
<Container>
// ...
<Button onClick={signOut}>Sign out</Button>
// ...
</Container>
)
}
export default LoggedIn;
and here is our _app.js :
import '../styles/globals.css'
import { AuthUserProvider } from '../context/AuthUserContext';
function MyApp({ Component, pageProps }) {
return <AuthUserProvider><Component {...pageProps} /></AuthUserProvider>
}
export default MyApp
under lib, we have the clientApp.js :
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
const FirebaseCredentials = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID
}
// if a Firebase instance doesn't exist, create one
if (!firebase.apps.length) {
firebase.initializeApp(FirebaseCredentials)
}
export default firebase;
we also have useFirebaseAuth.js :
import { useState, useEffect } from 'react'
import firebase from './clientApp';
const formatAuthUser = (user) => ({
uid: user.uid,
email: user.email
});
export default function useFirebaseAuth() {
const [loading, setLoading] = useState(true);
const [authUser, setAuthUser] = useState(null);
// listen for Firebase state change
const clear = () => {
setAuthUser(null);
setLoading(true);
};
const authStateChanged = async (authState) => {
if (!authState) {
setAuthUser(null)
setLoading(false)
return;
}
setLoading(true)
var formattedUser = formatAuthUser(authState);
setAuthUser(formattedUser);
setLoading(false);
};
const createUserWithEmailAndPassword = (email, password) =>
firebase.auth().createUserWithEmailAndPassword(email, password);
const signOut = () =>
firebase.auth().signOut().then(clear);
const signInWithEmailAndPassword = (email, password) =>
firebase.auth().signInWithEmailAndPassword(email, password);
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged(authStateChanged);
return () => unsubscribe();
}, []);
return {
authUser,
loading,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut
};
}
finally under the context directory, we have AuthUserContext.js :
import { createContext, useContext, Context } from 'react'
import useFirebaseAuth from '../lib/useFirebaseAuth';
const authUserContext = createContext({
authUser: null,
loading: true,
signInWithEmailAndPassword: async () => {},
createUserWithEmailAndPassword: async () => {},
signOut: async () => {}
});
export function AuthUserProvider({ children }) {
const auth = useFirebaseAuth();
return <authUserContext.Provider value={auth}>{children}</authUserContext.Provider>;
}
// custom hook to use the authUserContext and access authUser and loading
export const useAuth = () => useContext(authUserContext);
Conclusion
We have now seen how to create a project in Firebase and implement authentication in our project. We will see in our next article how to use firestore as a database for our project