Supabase & Next.js: Build A Secure Login Page
Hey guys! 👋 Building a secure and user-friendly login page is super important for any web app. In this guide, we're diving into how to create one using Supabase and Next.js. We'll cover everything from setting up your project to implementing robust authentication, ensuring a smooth and secure user experience. Let's get started, shall we?
Setting the Stage: Project Setup and Dependencies
Alright, first things first, let's get our environment ready. We'll be using Next.js for our frontend and Supabase for our backend and authentication. If you're new to these, no worries, I'll walk you through the steps!
Creating a Next.js App
First, let's create a new Next.js project. Open up your terminal and run the following command. This sets up a basic Next.js app with all the essentials. You can choose to use TypeScript or JavaScript; the examples in this guide will be in JavaScript, but the concepts apply to both.
npx create-next-app supabase-nextjs-login
cd supabase-nextjs-login
Installing Supabase Client
Next, install the Supabase JavaScript client, which will be our key to interacting with Supabase's services. This package will handle the communication between your frontend and your Supabase backend.
npm install @supabase/supabase-js
Setting Up Environment Variables
Now, let's configure our environment variables. You'll need your Supabase project's URL and API key. Go to your Supabase dashboard, navigate to the Settings section, and then to API. You'll find your project URL and anon key (public key) there. In your Next.js project, create a .env.local file in the root directory and add the following:
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
Make sure to replace YOUR_SUPABASE_URL and YOUR_SUPABASE_ANON_KEY with your actual Supabase credentials. Using NEXT_PUBLIC_ ensures these variables are accessible in your frontend code.
Building the Frontend: Login and Signup Forms
Okay, with the project set up, let's build the frontend. We'll create simple login and signup forms. This includes the UI elements, user input fields, and the basic structure for user interaction. These forms will be responsible for collecting user data.
Creating the Login Form
Create a new file called Login.js in your pages directory (or wherever you prefer to organize your components). This file will contain the logic for the login form. Here's a basic implementation:
// pages/Login.js
import { useState } from 'react';
import { useRouter } from 'next/router';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY);
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const router = useRouter();
const handleLogin = async (e) => {
e.preventDefault();
try {
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
if (error) {
setError(error.message);
console.error('Login error:', error);
} else {
console.log('Login successful:', data);
router.push('/'); // Redirect to homepage or dashboard
}
} catch (error) {
setError('An unexpected error occurred.');
console.error('Login failed:', error);
}
};
return (
<div>
<h1>Login</h1>
{error && <p style={{ color: 'red' }}>{error}</p>}
<form onSubmit={handleLogin}>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">Login</button>
</form>
</div>
);
}
This component includes state variables for email, password, and error messages. The handleLogin function is where we'll implement the authentication logic using the Supabase client.
Creating the Signup Form
Similarly, create a Signup.js file (or a component file of your choice) to handle the signup form.
// pages/Signup.js
import { useState } from 'react';
import { useRouter } from 'next/router';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY);
export default function Signup() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const router = useRouter();
const handleSignup = async (e) => {
e.preventDefault();
try {
const { data, error: signupError } = await supabase.auth.signUp({ email, password });
if (signupError) {
setError(signupError.message);
console.error('Signup error:', signupError);
} else {
console.log('Signup successful:', data);
router.push('/'); // Redirect after signup
}
} catch (error) {
setError('An unexpected error occurred.');
console.error('Signup failed:', error);
}
};
return (
<div>
<h1>Signup</h1>
{error && <p style={{ color: 'red' }}>{error}</p>}
<form onSubmit={handleSignup}>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">Signup</button>
</form>
</div>
);
}
This form is structured similarly to the login form but calls supabase.auth.signUp to create new user accounts.
Backend Authentication with Supabase
Now, let's wire up our frontend forms to Supabase. This section covers the crucial step of authenticating users using the Supabase client. This is where the magic happens and secures your application. This is also where you configure user authentication and handle their sessions.
Implementing Login Functionality
In the handleLogin function of your Login.js component, you'll use the Supabase client's signInWithPassword method. This method sends the user's email and password to Supabase for authentication. Here's how to integrate it:
const handleLogin = async (e) => {
e.preventDefault();
try {
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
if (error) {
setError(error.message);
console.error('Login error:', error);
} else {
console.log('Login successful:', data);
router.push('/'); // Redirect to homepage or dashboard
}
} catch (error) {
setError('An unexpected error occurred.');
console.error('Login failed:', error);
}
};
Implementing Signup Functionality
In the handleSignup function of your Signup.js component, you'll use the Supabase client's signUp method. This method creates a new user account in Supabase. Here’s how to do it:
const handleSignup = async (e) => {
e.preventDefault();
try {
const { data, error: signupError } = await supabase.auth.signUp({ email, password });
if (signupError) {
setError(signupError.message);
console.error('Signup error:', signupError);
} else {
console.log('Signup successful:', data);
router.push('/'); // Redirect after signup
}
} catch (error) {
setError('An unexpected error occurred.');
console.error('Signup failed:', error);
}
};
Handling Errors and User Feedback
Always provide clear error messages to the user. Use the setError state variable to display error messages. Catching errors in both login and signup flows is crucial for a great user experience. Make sure to log errors to the console for debugging, too.
Integrating with Next.js Pages and Routing
Once the user is authenticated, it's time to route them to the appropriate pages. This includes creating pages for login, signup, the home page, and a protected area (like a dashboard). Also, creating a navigation menu with links to these pages.
Creating the Login and Signup Pages
Create a pages/login.js file (if you haven't already) and a pages/signup.js file. These pages will render your login and signup forms, respectively. You can import the components you created earlier into these pages.
// pages/login.js
import Login from '../components/Login';
export default function LoginPage() {
return <Login />;
}
// pages/signup.js
import Signup from '../components/Signup';
export default function SignupPage() {
return <Signup />;
}
Setting Up Protected Routes
Create a protected route (e.g., a dashboard or profile page) that requires authentication. You'll use the supabase.auth.getSession() method to check if a user is logged in. If a user tries to access a protected route without being logged in, redirect them to the login page.
// pages/dashboard.js
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY);
export default function Dashboard() {
const router = useRouter();
useEffect(() => {
const checkUser = async () => {
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
router.replace('/login');
}
};
checkUser();
}, [router]);
return (
<div>
<h1>Dashboard</h1>
<p>Welcome to your dashboard!</p>
</div>
);
}
Implementing Logout Functionality
Add a logout button or link to allow users to sign out. Implement the supabase.auth.signOut() method to clear the user's session and redirect them to the login page.
// Example within your Navbar or Profile component
const handleLogout = async () => {
await supabase.auth.signOut();
router.push('/login');
};
Enhancements: User Experience and Security
Let's take our login page to the next level by focusing on user experience (UX) and security. These enhancements will make your app more user-friendly and more resistant to attacks. This section offers recommendations for how you can improve your login page.
Password Strength Validation
Implement password strength validation to encourage users to create strong passwords. Use a library like zxcvbn or a custom function to check password strength. This will help make your app more secure by increasing the likelihood of users choosing secure passwords. You can display a password strength indicator below the password input field. Provide clear feedback to users on the requirements for a strong password.
Email Verification
Enable email verification in Supabase to verify users' email addresses. This is a crucial security measure to prevent fake accounts and ensure users have access to their registered email. Supabase provides built-in support for email verification; enable it in your Supabase project settings. After signup, users receive a verification email and will only be able to log in once they've verified their account. This adds an extra layer of security.
Rate Limiting
Implement rate limiting to prevent brute-force attacks. Rate limiting restricts the number of login attempts from a single IP address within a specific time frame. You can use a library like express-rate-limit (if you're using a backend with Next.js API routes) or implement rate limiting at the server level (e.g., using a reverse proxy like Nginx). Set up rate limiting rules that block or slow down login attempts from the same IP address if there are too many failed attempts in a short period. This helps protect your application from automated attacks.
Error Handling and Feedback
Provide clear and helpful error messages to users. Don't just display generic error messages; instead, give users specific instructions on how to fix the problem (e.g.,