Full Example
I couldn't find a complete example of Next.js authentication with next-iron-session so I used all the little pieces I found online and created one to share.
The Code
Before talking about authentication we need to setup the model of the User. To do this we will be using mongoose for flexibility and ease of usage. You can ignore the usage of httpStatus and APIError. Here is the user.model.js inside a models folder at root level.
import mongoose from 'mongoose';
import httpStatus from 'http-status';
import APIError from '@/lib/APIError';
/**
* User Schema
*/
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: false,
lowercase: false,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
trim: true,
},
password: {
type: String,
required: true,
},
updatedAt: {
type: Date,
default: Date.now,
},
createdAt: {
type: Date,
default: Date.now,
},
});
/**
* Add your
* - pre-save hooks
* - validations
* - virtuals
*/
/**
* Methods
*/
UserSchema.method({});
/**
* Statics
*/
UserSchema.statics = {
/**
* Get user
* @param {string} id - The email of user.
* @returns {Promise<User, APIError>}
*/
get(email) {
return this.findOne({ email });
.select(
'name email'
)
.exec()
.then((user) => {
if (user) {
return user;
}
const err = new APIError(
'User id does not exist',
httpStatus.NOT_FOUND
);
return Promise.reject(err);
});
},
/**
* List users in descending order of 'createdAt' timestamp.
* @param {number} skip - Number of users to be skipped.
* @param {number} limipt - Limit number of users to be returned.
* @returns {Promise<User[]>}
*/
list({ skip = 0, limit = 50 } = {}) {
return this.find()
.select(
'name email createdAt updatedAt '
)
.sort({ createdAt: -1 })
.skip(+skip)
.limit(+limit)
.exec();
},
};
/**
* @typedef User
*/
export default mongoose.models.User || mongoose.model('User', UserSchema);
One important detail to see here is the last line, Next.js will try to load the same file creating a duplicate instance and will give an error without the statement.
export default mongoose.models.User || mongoose.model('User', UserSchema);
Config the Database
Create a file with the mongoose connection. Also see the part were the connection is saved in local memory to re use it. Created a file lib/dbConnect.js
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error(
'Please define the MONGODB_URI environment variable inside .env.local'
);
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*/
let cached = global.mongoose;
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
async function dbConnect() {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
useNewUrlParser: true,
useUnifiedTopology: true,
bufferCommands: false,
bufferMaxEntries: 0,
useFindAndModify: false,
useCreateIndex: true,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
cached.conn = await cached.promise;
return cached.conn;
}
export default dbConnect;
Auth Code
For the authentication we need first to create a user! So here is the React function to accomplish it. You can ignore the components that have the actual html.
import React, { useState } from 'react';
import { useRouter } from 'next/router';
import SignUpLayout from '@/components/SignUp/SignUpLayout';
import Form from '@/components/SignUp/Form';
import fetchJson from '@/lib/fetchJson';
export default function Register() {
const router = useRouter();
const [errorMsg, setErrorMsg] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const email = e.currentTarget.email.value;
const password = e.currentTarget.password.value;
const name = e.currentTarget.name.value;
try {
await fetchJson('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name }),
});
return router.push('/index');
} catch (error) {
setErrorMsg(error.data.message);
}
};
return (
<SignUpLayout>
<Form isLogin errorMessage={errorMsg} onSubmit={handleSubmit}></Form>
</SignUpLayout>
);
}
Now let's configure the session handler/guard. This is inside lib/session.js and this is the configuration that iron session will use to create the cookie.
// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
import { withIronSession } from 'next-iron-session'
export default function withSession(handler) {
return withIronSession(handler, {
password: process.env.SECRET_COOKIE_PASSWORD,
cookieName: 'cookie-name',
cookieOptions: {
// the next line allows to use the session in non-https environments like
// Next.js dev mode (http://localhost:3000)
maxAge: 60 * 60 * 24 * 30, // 30 days
secure: process.env.NODE_ENV === 'production' ? true : false,
},
})
}
Register
Now we can use the Next.js api inside pages/api/auth/register.js. This code includes de usage of the User model from mongoose, the database connection, and the user creation with the hashed password.
import dbConnect from '@/lib/dbConnect';
import User from '@/models/user.model';
import bcrypt from 'bcryptjs';
import httpStatus from 'http-status';
import withSession from '@/lib/session';
export default withSession(async (req, res) => {
const { name, email, password } = await req.body;
try {
if (req.method === 'POST') {
await dbConnect();
const userCheck = await User.findOne({ email: email.toLowerCase() });
if (userCheck) {
return res.status(httpStatus.BAD_REQUEST).json({ message: 'User already exists' });
}
// create user
const hashPassword = await bcrypt.hash(password, 10);
const user = await new User({
name,
email,
password: hashPassword,
});
await user.save();
req.session.set('user', { id: user._id, email: user.email });
await req.session.save();
return res.status(httpStatus.OK).end();
}
return res.status(httpStatus.BAD_REQUEST).end();
} catch (error) {
console.log(error, error.message);
const { response: fetchResponse } = error;
res.status(fetchResponse?.status || 500).json(error.message);
}
});
Remember to set the user in the session request with only the required fields and not the whole user. We don't want to save the users' password in the cookie, right?
req.session.set('user', { id: user._id, email: user.email });
You can remove the cookie creation if you don't want to login automatically the user after its creation.
Login
Now for the login, first we will be creating a custom hook, so we can check if the user is logged in and redirect if it is the case. So here is the hook. This hook also uses the useSWR that the official Next.js suggests to use.
import { useEffect } from 'react'
import Router from 'next/router'
import useSWR from 'swr'
export default function useUser({
redirectTo = false,
redirectIfFound = false,
} = {}) {
const { data: user, mutate: mutateUser } = useSWR('/api/user')
useEffect(() => {
// if no redirect needed, just return (example: already on /dashboard)
// if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
if (!redirectTo || !user) return
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !user?.isLoggedIn) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && user?.isLoggedIn)
) {
Router.push(redirectTo)
}
}, [user, redirectIfFound, redirectTo])
return { user, mutateUser }
}
The hook queries the session saved in the request like this: (this is the pages/api/user.js).
import withSession from '@/lib/session';
export default withSession(async (req, res) => {
const user = req.session.get('user');
if (user) {
res.json({
isLoggedIn: true,
...user,
});
} else {
res.json({
isLoggedIn: false,
});
}
});
Now we can proceed to the React Login function.
import { useState } from "react";
import Form from "@/components/Login/Form";
import LoginLayout from "@/components/Login/LoginLayout";
import useUser from '@/lib/useUser';
import fetchJson from '@/lib/fetchJson';
// layout for page
export default function Login() {
const { mutateUser } = useUser({
redirectTo: '/',
redirectIfFound: true,
});
const [errorMsg, setErrorMsg] = useState('');
async function handleSubmit(e) {
e.preventDefault();
const body = {
email: e.currentTarget.email.value,
password: e.currentTarget.password.value
};
try {
mutateUser(
await fetchJson('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
);
} catch (error) {
console.error('An unexpected error happened:', error);
setErrorMsg(error.data.message);
}
}
return (
<>
<LoginLayout>
<Form isLogin errorMessage={errorMsg} onSubmit={handleSubmit}></Form>
</LoginLayout>
</>
);
}
And here is the pages/api/auth/login.js API call that uses the mongoose connection and bcrypt validation.
import withSession from '@/lib/session';
import User from '@/models/user.model';
import bcrypt from 'bcryptjs';
import httpStatus from 'http-status';
import dbConnect from '@/lib/dbConnect';
export default withSession(async (req, res) => {
const { email, password } = await req.body;
try {
await dbConnect();
// get user from db
const user = await User.findOne({ email: email.toLowerCase() });
if (!user) {
// password not valid
return res.status(httpStatus.UNAUTHORIZED).json({ message: 'User does not exist'});
}
// compare hashed password
const valid = await bcrypt.compare(password, user.password);
// if the password is a match
if (valid === true) {
req.session.set('user', { id: user._id, email: user.email });
await req.session.save();
return res.json(user);
} else {
// password not valid
return res.status(httpStatus.UNAUTHORIZED).json({ message: 'Invalid Password'});
}
} catch (error) {
console.log(error);
const { response: fetchResponse } = error;
res.status(fetchResponse?.status || 500).json(error.data);
}
});
Top comments (2)
Thank you
hi! this is really interesting, thanks! I was wondering if you have also a github repo so to check the missing pieces...