DEV Community

Cover image for Create A Passwordless Authentication System
Kinanee Samson
Kinanee Samson

Posted on

Create A Passwordless Authentication System

There is a new way of building an authentication into your backend application. This approach eliminates the need for a password, giving rise to the term passwordless authentication. Passwordless authentication systems are becoming more popular because they offer several benefits over traditional passwords based systems. For example, they can provide a better user experience, reduce the risk of data breaches, and lower the cost of password management.

A passwordless authentication system can be implemented in a number of ways, including by employing biometric information, one-time codes, or public-private key cryptography. The best option will rely on the particular requirements of the system and the users. Each method has advantages and disadvantages. For this article we would use a one-time code, which will be sent to the users email.

First let's spin up our npm project, run npm init --y, then install our dependencies, for this app we would use express.

$ npm i express
Enter fullscreen mode Exit fullscreen mode

let's create our app, and get our server runing.

// index.ts
import express from 'express';

const app = express();

app.listen(3000, () => console.log(`server running on port 3000`))
Enter fullscreen mode Exit fullscreen mode

Next we'll set up our database with Mongoose, to do this first we have to install Mongoose.

$ npm i mongoose
Enter fullscreen mode Exit fullscreen mode

We have to setup our mongoose schema and models, which we will do below;

// user.schema.js

import { Schema } from 'mongoose';

export const UserSchema = new Schema({
  email: {
    type: String,
    required: [true, "Please provide your email"],
    unique: true
  },
  fistName: String,
  lastName: String,
  otp: number;
}, {
  timestamps: true
})
Enter fullscreen mode Exit fullscreen mode

We have declared a userschema above, the user schema defines some basic properties whic a user should have, most importantly the email which we set to be required for all users, we also ensure that two users cannot have the same emails by ensuring that emails are unique. Let's create the user model to handle all of the logic.

// user.model.js

import { UserSchema } from './user.schema';
import { model } from 'mongoose';

UserSchema.statics.createAccount = async function ({
  email, 
  firstName,
  lastName
}) {
  try {
    const user = await this.create({ 
      email,
      firstName,
      lastName
    })
    // Generate OTP for user
    const otp = Math.floor(Math.random() * 1000000);
    await user.updateOne({ otp });
    sendEmailSomehow(`Your otp is ${otp}`)
    return [user, null]
  } catch(error) {
    return [null, error.message]
  }
}

Enter fullscreen mode Exit fullscreen mode

The snippet above creates a new user for us, we generate an otp which will serve as the one time pass code which we will use to verify the user, we use an email service to send the user an email, I will assume that you have a third party service that will handle email delivery.


UserSchema.statics.login = async function (email) {
  try {
    const user = await this.findOne({ email });
    const otp = Math.floor(Math.random() * 1000000);
    await user.updateOne({ otp });
    sendEmailSomehow(`Your otp is ${otp}`);
    return [user, null]
  } catch (error) {
     return [null, error.message]
  }
}

Enter fullscreen mode Exit fullscreen mode

This snippet above handles the login process, we check to find a user with an email that matches the one provided, if there is a user we generate an otp for the user then send it to their email. Let's write the function that will validate their otp.


UserSchema.statics.verifyOTP = async function (otp, email) {
  try {
    const user = await this.findOne({ email, otp });
    return [user, null];
  } catch (error) {
    return [null, error.message]
  }
}

export const Users = await model('user', UserSchema);
Enter fullscreen mode Exit fullscreen mode

Now we have a basic setup in place, we need to import the Users model inside our controllers which we will create below.

// user.controller.js

import { Users } from 'user.model';

export const createAccount = async (req, res) => {
  const { firstName, lastName, email } = req.body;
  const [user, err] = await Users.createAccount({
    firstName,
    lastName,
    email,
  });
  if (err) res.status(400).json({ error: err })
  res.status(201).json({ user })
}

Enter fullscreen mode Exit fullscreen mode

We have the first controller which handles user registration set up, below we set up the other controllers for login in and verifying our user token.


export const login = async (req, res) => {
  const { email } = req.body;
  const [user, err] = await Users.login(email);
  if (err) res.status(400).json({ error: err });
  res.status(201).json({ user });
}

export const verifyOTP = async (req, res) => {
  const { otp } = req.body;
  const [user, err] = await Users.verifyOTP(otp);
  if (err) res.status(400).json({ error: err });
  res.status(201).json({ user });
}
Enter fullscreen mode Exit fullscreen mode

We have created and exported our controller functions what we need to do is to map each controller function to a route, this will be done inside a route file.

// router.js
import { createAccount, login, verifyOTP } from './user.controller';
import { Router } from 'express';

export const router = Router();

router.post('/register', createAccount);
router.post('/login', login);
router.post('/otp', verifyOTP);

Enter fullscreen mode Exit fullscreen mode

Our application is pretty done what we need to do now is to import our router and apply it to our application.

// index.js continued
import { router } from 'router';

app.use(router);

Enter fullscreen mode Exit fullscreen mode

Kick start your application and you have a basic setup for a password less authentication system, would you like to see how we could do this but with TypeORM and MySQL as our database and fastify as our sever framework. See you in the next article

Latest comments (3)

Collapse
 
inizio profile image
IniZio

This lack a lot of consideration for practical use case. What if user login with multiple devices? What if user want to resend an OTP? Does the OTP has an expiration date?

Collapse
 
nanaaikinson24 profile image
nanaaikinson24 • Edited

The example above is just a blueprint/skeleton. It is up to the developer(s) to add flesh to the skeleton.

Collapse
 
artydev profile image
artydev

Great, thank you :-)