npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@baijanstack/express-auth

v0.0.0-alpha.41

Published

> ! This library is under active development. Please do not use it in production.

Downloads

119

Readme

express-auth

! This library is under active development. Please do not use it in production.

This is an authentication library for express. It uses email-password authentication flow for authentication.

Motivation

The motivation behind creating this library is to reduce the boilerplate code for authentication that we have to write when implementing authentication in any express application.

Principles

We have used two principles to implement this library.

  • One is to generate routes for authentication.
  • The other is to provide the interface for the persistence layer.

The route generator takes care of generating the routes for authentication. The persistence layer provides the interface for the persistence layer which is implemented by the user.

Generated Routes

The following routes are generated for authentication.

/signup

This route handles sign up of new user.

Request Body must have email and password properties:

{
  "email": "[email protected]",
  "password": "baijan",
  "name": "baijan"
}

When you sign up, we will hash the password and *send the email for verification. We will store the hashed password in the storage using the implementation provided.

POST /verify-email

This route is used to verify the email after signing up.

Note: Before making request to this route, you need to make a request to /send-otp route to send the OTP.

Request Body must have email and otp properties:

{
  "email": "[email protected]",
  "otp": "123456"
}

POST /login

This route handles login of user.

Request Body must have email and password properties:

{
  "email": "[email protected]",
  "password": "baijan"
}

POST /logout

This route log outs user from the application. We will invalidate the refresh token.

POST /refresh

This route refreshes the access token and refresh token if refresh token is valid.

GET /me

This route returns the details of logged in user.

POST /reset-password

This route resets the password of the logged in user.

POST /forgot-password

This route is used to change the password of the logged in user.

Note: Before making request to this route, you need to make a request to /send-otp route to send the OTP.

Request Body must have email property:

{
  "email": "[email protected]",
  "otp": "123456"
}

/send-otp

This route is used to update the password. User must send the new password and OTP obtained in the email.

Request Body must have email property:

{
  "email": "[email protected]"
}

Usage

  • Install the dependency.
npm install @baijanstack/express-auth
  • Create the auth configuration
import { TConfig } from '@baijanstack/express-auth';

const authConfig: TConfig = {
  BASE_PATH: '/v1/auth', // base path for authentication
  SALT_ROUNDS: 10, // number of rounds for password hashing
  TOKEN_SECRET: 'random_secure_secret_value', // secret for token generation
  ACCESS_TOKEN_AGE: 60000, // age of access token in milliseconds
  REFRESH_TOKEN_AGE: 240000, // age of refresh token in milliseconds
  EMAIL_VERIFICATION_TOKEN_AGE: 300000, // age of email verification token in milliseconds
};
  • Implement the INotifyService for sending notifications. We will use EmailNotificationService for sending notifications for different events such as TOKEN_STOLEN, OTP and EMAIL_VERIFIED.
// notifier.ts
import { INotifyService } from '@baijanstack/express-auth';

export class EmailNotificationService implements INotifyService {
  async sendTokenStolen(email: string): Promise<void> {
    console.log(`Notifying | TOKEN_STOLEN | Email: ${email}`);
  }
  async sendOtp(email: string, payload: { code: string; generatedAt: number }): Promise<void> {
    console.log(`Notifying | OTP | Email: ${email}`, payload);
  }
  async notifyEmailVerified(email: string): Promise<void> {
    console.log(`Notifying | EMAIL_VERIFIED | Email: ${email}`);
  }
}
  • Create an instance of the route generator.

You need to pass the INotifyService and TConfig to the route generator implementations.

import express from 'express';
import { initAuth, RouteGenerator, TConfig } from '@baijanstack/express-auth';

const app = express();

const routeGenerator = new RouteGenerator(app, notificationService, authConfig);
  • Initiate the auth library.
import { initAuth } from '@baijanstack/express-auth';

initAuth({
  routeGenerator,
  // ... need to import all the handlers from
  // `handlers.ts` file that we will make in next section
  signUpHandler: new SignUpHandler(),
  loginHandler: new LoginHandler(),
  logoutHandler: new LogoutHandler(),
  refreshHandler: new RefreshHandler(),
  resetPasswordHandler: new ResetPasswordHandler(),
  meRouteHandler: new MeRouteHandler(),
  verifyEmailHandler: new VerifyEmailHandler(),
  forgotPasswordHandler: new ForgotPasswordHandler(),
  verifyOtpHandler: new VerifyOtpHandler(),
});
  • When you initiate the auth library, you need to pass handlers for each route. The handlers are independent of storage type - in-memory or database etc.

I will show you how to implement these handlers in the next section using in-memory storage.

Note: You can see an implementation of the handlers using prisma in Sample Auth Example.

In-memory handlers

// handlers.ts
import { ISignUpHandler, ILoginHandler, ILogoutHandler, IRefreshHandler, IResetPasswordHandler, IMeRouteHandler, IVerifyEmailHandler, IForgotPasswordHandler, ISendOtpHandler } from '@baijanstack/express-auth';

export type TUser = {
  name: string;
  email: string;
  password: string;
  is_email_verified: boolean;
  otps: {
    code: string;
    generatedAt: number;
  }[];
};

const users: TUser[] = [];

type TEmailObj = {
  email: string;
};

interface TSignUpBodyInput extends TEmailObj {
  name: string;
  password: string;
}

export class SignUpHandler implements ISignUpHandler {
  constructor() {
    console.log('signup persistor init...');
  }

  errors: { USER_ALREADY_EXISTS_MESSAGE?: string } = {};

  doesUserExists: (body: TSignUpBodyInput) => Promise<boolean> = async (body) => {
    const user = users.find((user) => user.email === body.email);
    return !!user;
  };

  saveUser: (body: TSignUpBodyInput, hashedPassword: string) => Promise<void> = async (body, hashedPassword) => {
    users.push({
      name: body.name,
      email: body.email,
      password: hashedPassword,
      is_email_verified: false,
      otps: [],
    });
  };
}

export class LoginHandler implements ILoginHandler {
  getUserByEmail: (email: string) => Promise<TUser | null> = async (email) => {
    const user = await users.find((user) => user.email === email);

    if (!user) {
      return null;
    }

    return user;
  };
  errors: { PASSWORD_OR_EMAIL_INCORRECT?: string } = {
    PASSWORD_OR_EMAIL_INCORRECT: 'Password or email incorrect',
  };

  getTokenPayload: (email: string) => Promise<{
    name: string;
    email: string;
  } | null> = async (email) => {
    const user = users.find((user) => user.email === email);

    if (!user) {
      return null;
    }

    return {
      email: user?.email,
      name: user?.name,
    };
  };
}

export class LogoutHandler implements ILogoutHandler {
  shouldLogout: () => Promise<boolean> = async () => {
    return true;
  };
}

export class RefreshHandler implements IRefreshHandler {
  errors: { INVALID_REFRESH_TOKEN?: string } = {};

  refresh: (token: string) => Promise<void> = async () => {
    console.log('refreshing token...');
  };

  getTokenPayload: (email: string) => Promise<{
    name: string;
    email: string;
  } | null> = async (email) => {
    const user = users.find((user) => user.email === email);

    if (!user) {
      return null;
    }

    return {
      email: user?.email,
      name: user?.name,
    };
  };
}

export class ResetPasswordHandler implements IResetPasswordHandler {
  saveHashedPassword: (email: string, hashedPassword: string) => Promise<void> = async (email, hashedPassword) => {
    const userIdx = users.findIndex((user) => user.email === email);
    if (userIdx < 0) {
      throw new Error(`User not found`);
    }

    users[userIdx].password = hashedPassword;
  };
  getOldPasswordHash: (email: string) => Promise<string> = async (email) => {
    const user = users.find((user) => user.email === email);
    if (!user) {
      return '';
    }
    return user.password;
  };
}

export class MeRouteHandler implements IMeRouteHandler {
  getMeByEmail: (email: string) => Promise<{ email: string; name: string } | null> = async (email) => {
    const user = users.find((user) => user.email === email);

    if (!user) {
      return null;
    }

    return {
      name: user?.name,
      email: user?.email,
    };
  };
}

export class VerifyEmailHandler implements IVerifyEmailHandler {
  isOtpValid: (email: string, otp: string) => Promise<boolean> = async (email, otp) => {
    const user = users.find((user) => user.email === email);
    if (!user) {
      return false;
    }
    const lastOtp = user.otps[user.otps.length - 1];

    const isOtpMatched = lastOtp?.code === otp;

    const isExpired = lastOtp?.generatedAt < Date.now() / 1000 - 60 * 5; // 5 minutes

    return isOtpMatched && !isExpired;
  };

  isEmailAlreadyVerified: (email: string) => Promise<boolean> = async (email) => {
    const user = users.find((user) => user.email === email);

    return !user?.is_email_verified;
  };
}

export class SendOtpHandler implements ISendOtpHandler {
  doesUserExists: (email: string) => Promise<boolean> = async (email) => {
    const user = users.find((user) => user.email === email);
    return !!user;
  };

  saveOtp: (email: string, otp: { code: string; generatedAt: number }) => Promise<void> = async (email, otp) => {
    const userIdx = users.findIndex((user) => user.email === email);
    if (userIdx < 0) {
      throw new Error(`User not found`);
    }
    users[userIdx].otps.push(otp);
  };
}

export class ForgotPasswordHandler implements IForgotPasswordHandler {
  isOtpValid: (email: string, otp: string) => Promise<boolean> = async (email, otp) => {
    const user = users.find((user) => user.email === email);
    if (!user) {
      return false;
    }
    const lastOtp = user.otps[user.otps.length - 1];

    const isOtpMatched = lastOtp?.code === otp;

    const isExpired = lastOtp?.generatedAt < Date.now() / 1000 - 60 * 5; // 5 minutes

    return isOtpMatched && !isExpired;
  };

  saveNewPassword: (email: string, password: string) => Promise<void> = async (email, password) => {
    const userIdx = users.findIndex((user) => user.email === email);
    if (userIdx < 0) {
      throw new Error(`User not found`);
    }
    users[userIdx].password = password;
  };
}

Protected Routes

You can protect your routes by using the middlewares provided by this library.

The routeGenerator has a middleware to protect your routes.

app.get('/protected', routerGenerator.validateAccessToken, (req, res) => {
  console.log('Logged in user is:', req.user);
  res.send('Hello World');
});

Collaborators