Password-Based Authentication in Next.js
- Category
- Guides
- Published
This article explores password authentication, risks, and better solutions like SSO, MFA, and passwordless login.
Passwords. The best and worst thing that ever happened to internet security. We talk about passwordless and SSO and magic links and MFA, but 98% of the world’s websites only accept password authentication.
It is expected that a site will allow you to enter your own username/email and a password to sign up and log in. Even though we know there are security flaws with this approach and much better ways to build authentication, your site has to have password-based authentication, otherwise it looks… odd.
So how do you do that? Here we’re going to show you. But this is a demonstration of the internal workings of password authentication to give you an understanding of how it works. This isn’t something you should truly build yourself if you care about your users. Use libraries and services built by experts if you are going to implement password authentication in your app–we show you how to do this with Clerk at the end.
Building passwords from basics in Next.js
You need four components to build password authentication:
- A frontend where the user can enter their username and password to sign up or log in
- A backend to deal with the signup/login logic
- A way to hash the password so it can be stored securely
- A storage mechanism (and logic) to save the username and hashed password
Parts 1-3 are are possible with Next.js. For storage, we’ll be using Supabase. Supabase is an open source Firebase alternative that provides a large selection of database management tools. It integrates with a large number of modern SaaS tools, including Clerk. Here, we won’t be using the Supabase libraries. Instead, as under the hood Supabase is a Postgres database, we’ll use the low-level methods to connect and then use SQL to load and recall our data.
Let’s start with creating a new Next.js application:
We’ll only be using two extra libraries in this build:
- bcrypt: Bcrypt is a password hashing function which incorporates a salt to protect against rainbow table attacks.
- pg: Pg (short for “node-postgres”) is a collection of Node.js modules for interfacing with PostgreSQL databases, offering features such as connection pooling and prepared statements.
You can install them with npm install:
Storage
We’ll actually work backwards from our list above and start with our database functionality. As we said, we’re using Supabase. Supabase is an open-source Firebase alternative that provides developers with a suite of tools and services for building serverless applications. It offers real-time databases, instant APIs, and authentication and authorization functionalities.
Here, we’re only going to use the database component, and even then we won’t use the Supabase libraries. Instead, we’ll just use the general Postgres account information to connect directly to the database.
Sign up for Supabase and create a new project. The project can be called anything, but take note of the password you create as you will need that for the connection. The project will take a few minutes to spool up, but once it has, go to “Database → Table → new table” to create a new table. You’ll only need two fields in this table:
- Username - Text
- Password - Text
You can keep the id
and created_at
fields. Make sure, for the purposes of this demo at least, you turn Row Level Security off as otherwise it’ll be much more difficult to access the database.
Row-Level Security (RLS) is a feature that allows fine-grained control over which rows in a table can be accessed and modified by users. With RLS, the database admin can define policies that restrict access to certain rows based on the attributes of the user or the data. If you are building for production, RLS is a must.
With the table created, head to Project Settings -> Database. You’ll see “Connection info.” This is what we’ll need to connect to this database:
With that information (and the password you created earlier), we can now start building the code and logic for our password authentication.
Open up your password-app directory in your favorite IDE and add a “utils” directory at the root. Within utils, create a db.js file that will handle your database connection:
Fill in the User
, Host
, Database name
, and Password
with the “Connection info” from Supabase.
(Like with RLS, don’t do it this way if you are deploying to production–never hardcode variables like this in your code. Instead save them as environmental variables and import them via process.env).
Hashing and salting
With our database connection sorted, we can start to create the backend logic we need to route our user requests and deal with the username and password.
There should already be a pages/api directory. Within that create a signup.js file. Add this code to the file:
Here’s a breakdown of what the code does:
import pool from ‘../../db’
imports thepool
object fromdb.js
which is a pool of database connections we’re using to connect to Postgresimport bcrypt from ‘bcrypt’
imports thebcrypt
library, which is a password-hashing function. You’ll use it to securely hash passwords before storing them in the database.export default async function signup
defines an asynchronous function calledsignup
which is the main function of this module.if (req.method === ‘POST’)
checks if the HTTP request method is POST. If it’s not, it returns a 405 status code.const { username, password } = req.body
destructures the request body to extract the username and password properties from the request body.const hashedPassword = await bcrypt.hash(password, 10)
usesbcrypt
to hash the user’s password asynchronously with a salt round of 10. The hashed password is then stored in hashedPassword.const result = await pool.query(…)
sends a SQL query to the Supabase to insert a new row into theusers
table. It inserts the username and the hashed password.’INSERT INTO users(username, password) VALUES($1, $2) RETURNING *’
is the SQL query being sent to the database. It’s parameterized to prevent SQL injection attacks. The$1
and$2
are placeholders for theusername
andhashedPassword
that will be inserted.- The final lines are logic for catching errors or returning success messages.
There’s a lot there, but basically when this API route is called it takes the username and password the user entered, hashes and salts the password, and then saves them in the database.
What are hashing and salting?
- Hashing: Hashing is the process of converting an input of any length into a fixed size string of text, using a mathematical algorithm. Hashing is often used to securely store sensitive data such as passwords. Even a small change in the input text will produce a drastic change in the output hash, making it computationally infeasible to derive the original input from the hashed output.
- Salting: Salting is a technique used in conjunction with hashing to increase the security of stored passwords. A salt is a random piece of data generated for each user that is added to the password before it is hashed. This means that even if two users have the same password, their hashed passwords will be different because the salts are different. Salting helps protect against rainbow table attacks, where an attacker pre-computes the hash values for possible passwords and looks for matches with hashed passwords.
All that happens in the line const hashedPassword = await bcrypt.hash(password, 10)
. Doing this is fundamental to password security. Without it, passwords would be saved in plain text, and anyone having access to the database would be able to retrieve everyone’s passwords. With hashing and salting, what is saved in the database isn’t the raw password.
Now that we can sign up, let’s create a file called pages/api/login.js so we can also log in. In this file, you would fetch the user from the database, compare the hashed passwords, and if they match, return a success:
This is very similar to the previous route, but instead of hashing and then storing, we’re retrieving and then comparing the stored hash to a hash of the password the user tried with the login page.
We retrieve the saved password from Supabase with const user = await pool.query(‘SELECT * FROM users WHERE username = $1’, [username])
. This grabs the user with the username just entered by the user.
We then use const passwordMatches = await bcrypt.compare(password, user.rows[0].password)
to compare the entered password with the retrieved password. bcrypt.compare
is a function that takes a plain-text input and a hashed string as arguments, and returns true if the plain-text input, when hashed with the same salt as the hashed string, matches the hashed string, thus verifying the authenticity of the input.
Creating the frontend with Next.js
All we need now is some logic on the frontend for the user to interact with. First, we’ll create a Profile component in components/profile.jsx:
In this component, we simply display a welcome message with the user’s username. Then we’ll remove the current Next.js boilerplate from index.js and add this code to include a login/signup form and the profile display.
In this component, we have a form with fields for username and password, and two buttons for signup and login. When either button is clicked, the handleSubmit
function is called with the corresponding path. If the request is successful, we update the isLoggedIn
and user
states, which will cause the Profile
component to be rendered.
So the user sees this (very simple) form:
If they sign up/log in correctly, they get to their profile page:
On the backend, we can see the new user in our database in Supabase, with their hashed and salted password:
Extra security
This is a basic example and does not handle all the edge cases you might need to cover in a production app, such as:
- Checking for unique usernames or emails during signup. For instance, the hashing and database retrieval depends on unique usernames.
- Handling password resets.
- Adding email verification.
You can check whether a username exists by running an additional query on the database before you try and select the user:
Here, you’ll get an error message if the user already exists.
Handling password resets and email verification are much more difficult. To handle password resets, you typically need to do the following:
- Generate a unique token for the password reset request.
- Associate this token with the user in your database, and set an expiry time for it.
- Send an email to the user with a link containing this token.
- When the user clicks the link, verify the token and its expiry time.
- If the token is valid, allow the user to enter a new password.
Email verification is similar, but instead of adding a new password at the end, you’ll have a verified boolean on the user that you set to true.
We also haven’t added any ability to force stronger passwords on users. You can do that through using a library such as the validator library. The validator library provides a collection of string validation and sanitization methods, simplifying data validation tasks in the server side, client side, or even for data stored in the database:
Here we can set that the password must have eight characters and one uppercase letter, one lowercase, one number, and one symbol.
There is a lot to think about to just do the basics of password authentication. Don’t want to do all that?
Using Clerk with Next.js for password authentication
Clerk allows you to quickly and easily add password authentication to Next.js and has both client and server side components.
Before we get to the code, we first want to set up our application to use email and password authentication in the Clerk dashboard. Head to your dashboard and select “Email, Phone, Username” under the “User & Authentication” menu. Make sure email is selected:
You can see you can make this required and used for sign-in, which is what we want here. We can also, with a quick toggle rather than dozens of lines of JavaScript, say that we want email verification.
Then, in the same menu, find the “Authentication factors” options and select “Password.” Again here you can easily select to force the user to use a more secure password:
Now we can start with the code. Let’s create a new Next.js app:
Now we can install Clerk:
Next, we want to create our environment variables for our Clerk API keys and routes. Create a .env.local file
in the root directory and add your keys and these routes (that we’re going to create in a moment):
With the keys and routes in place, we can add wrapper. This provides an active session and user context to Clerk’s hooks and other components. We want to wrap the entire to enable the context to be accessible anywhere within the app, so we put it in our _app.jsx
file:
We’re also going to add some middleware, which is the function that decides which pages are protected. Here, we’re going to protect everything. Add a middleware.js file to the root directory with this code:
We’ll now create our sign up and sign in pages. To create the sign up page, create a file at page/signup/[[…index]].jsx:
The [[…index]] syntax defines a catch-all route, so anything the user entered under signup (e.g. signup/a or signup/b) will still go to this page.
We’ll do the same for signing in, with the page at page/signin/[[…index]].jsx:
Finally, all we need is a button on the client to sign up and log in:
Now when we npm run dev
, we’ll get the Clerk sign in modal:
As we don’t have an account, we’ll switch to the sign up option:
We can then enter our email address and password. We have email verification turned on, so we have to go and click the link in our email. After that we’ll be redirected to our content:
Easier passwords in Next.js
That’s it. You now have password authentication set up with the necessary security features. Clerk offers custom password flows so you can design the sign in flow for your app as you want
People expect passwords on their sites. They aren’t going anywhere soon. So if you are creating a new site and want password authentication, use a well-designed library or service rather than creating your own–you and your users will be grateful.
Ready to get started?
Sign up today