Many of us struggling with remembering passwords for every sites we signed up so creating a passwordless authentication could serve a pretty nice user experience in some use cases.
There are perfect solutions to achieve this with third party services but in this post I would like to cover a self-made example.
The technologies and main packages I will use are:
- Next.js
- mongodb
- jsonwebtoken
- cookie
- nodemailer
Notice: As I want to keep it short I will not cover the signup scenario here but it's similar. Also won't cover the frontend scenario (sending the requests, waiting for the answers and handle them) that could be covered by another post.
The main idea is in order:
- the user wants to sign in and sends his/her email address (without password)
- an API route catches the email address and checks if that exists in the Database.
- the server creates a JWT token for the user and sends an email with it.
- the user clicks the link in the email which hits another api route: This one checks if the token is valid. If it is it sets an HttpOnly Cookie to the browser and redirects the user to the desired page.
The result: The user is safely logged in until he/she logges out or the token expires.
Let's see the flow in details
First we need set up a Next.js project and connect it to a database. I won't cover the project set up here please check the Next.js docs how to do that. Once we have a project up and running connect that to mongodb
. To do that I found this article from mongodb very useful:
How to Integrate MongoDB Into Your Next.js App
Don't forget to install the remaining packages:
npm install jsonwebtoken cookie nodemailer
After we have that - consider we have at least one user in the database who wants to sign in π
imagine this is mongodb
:
{
"_id": "606101c0af993c79f24a52d2",
"email": "user@example.com"
}
Look, there is no password or an enormous hash! π€
We need an API endpoint to catch the email address and send an email.
pages/api/auth/login.js
import jwt from 'jsonwebtoken';
import { connectToDatabase } from '..../mongodb';
import { sendEmail } from '..../server/sendEmail';
// We need a secret on there server side for the tokens
const { JWT_SIGNATURE } = process.env;
export default async function login(req, res) {
// pls check the mongodb article above for details
const { db } = await connectToDatabase();
const { email } = req.body;
try {
const user = await db.collection('users').findOne({ email });
if (user._id){
// We found the user registered, let's create a token!
const payload = { userId: user._id, email: user.email };
const token = jwt.sign(payload, JWT_SIGNATURE);
// We have the token let's email it!
const messageId = await sendEmail(email, token);
res
.status(200)
.json({ message: `Email was sent! id: ${messageId}` });
} else {
res
.status(401)
.json({ message: 'This email was not registered yet.' })
}
} catch(err) {
console.error(err);
}
}
Set up nodemailer for sending emails
This is just a basic setup you can of course implement more styling and separate the config in different files.
server/sendEmail.js
import nodemailer from "nodemailer";
export async function sendEmail(email, token) {
const href = `https://frontend.com/api/auth/verify?token=${token}`;
try {
let transporter = nodemailer.createTransport({
host: process.env.EMAIL_SERVER_HOST,
port: process.env.EMAIL_SERVER_PORT,
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD,
},
});
let info = await transporter.sendMail({
from: process.env.EMAIL_FROM,
to: email,
subject: 'Here is your login link! π₯³',
html: `
<h1>Hello/</h1>
<p>Please click <a href={href}>here</a> to sign in!</p>
`;
});
console.log("Message sent: %s", info.messageId);
return info.messageId;
} catch (e) {
console.error;
}
}
We need an API endpoint to wait for this token and sign in the user
pages/api/auth/verify.js
import cookie from 'cookie';
import jwt from 'jsonwebtoken';
const { JWT_SIGNATURE } = process.env;
export default async function verify(req, res) {
const { token } = req.query;
jwt.verify(token, JWT_SIGNATURE, (err, decoded) => {
if (err) {
res.status(401).json({ message: 'Token expired / invalid' });
} else {
res.setHeader(
'Set-Cookie',
cookie.serialize('anyCookieName', decoded.userId, {
path: '/',
httpOnly: true,
maxAge: 60 * 60 * 24 * 7, // 1 week
secure: process.env.NODE_ENV !== 'development',
})
);
res.status(200).redirect('https://frontend.com');
}
});
}
The user is signed in! β¨
After this any time a request hits an endpoint from this browser the HTTP Cookies
travels with them so you can capture it and check the user against the database.
export default async function anyEndpoint(req, res) {
const userId = req.cookies.anyCookieName;
// ...
}
You got the idea.
Conclusion
β Β Implementing a passwordless authentication is fun and hopefully also very convenient for the end users. You cannot lose the passwords they cannot leak either. You don't need to handle password resets.
β Β This tokens (~sessions) could live in the users' browser really long as they are HttpOnly Cookies so cannot accessed by browser extensions or any client side javascript. Pretty safe.
π You should always go to the email client to sign in.
If you want to logout remotely you can implement an accessToken
/ refreshToken
session based version which could be covered by another post π
Hope you enjoyed! βοΈ
More thoughts:
Thoughts on Passwordless Authentication
Klee Thomas γ» Jul 13 '20
Other techs:
Top comments (2)
Niceππ»ππ»
Thank you @kosztolanyi !