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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@khni/auth

v3.0.0

Published

A modular, extensible authentication system for Node.js and TypeScript with support for various authentication strategies, token management, and user session handling.

Readme

@khni/auth - Complete Authentication System

A robust, type-safe local authentication service for Node.js applications with complete token management, including access tokens, refresh tokens, and secure authentication flows.

🚀 Features

  • 🔐 Secure Authentication - Password hashing with bcrypt and configurable hashers
  • 📧 Multi-Identifier Support - Email and phone number authentication
  • 🔄 Token Management - Complete JWT access token and refresh token system
  • 🛡️ Type-Safe - Full TypeScript support with generic types
  • 📖 Comprehensive API - Complete authentication flow (register, login, refresh, logout)
  • 🧪 Fully Tested - 100% test coverage with Vitest
  • 📚 Well Documented - JSDoc documentation compatible with API Extractor
  • 🎯 Error Handling - Domain-specific and unexpected error handling

📦 Installation

npm install @khni/auth
pnpm add @khni/auth
yarn add @khni/auth

🏗️ Architecture

Complete Authentication System

The service provides a complete authentication system combining local authentication with token management:

┌─────────────────┐    ┌──────────────────┐    ┌──────────────┐
│ LocalAuthService│ ── │   IUserRepository   │ ── │ Your User DB │
└─────────────────┘    └──────────────────┘    └──────────────┘
         │                       │
         │                       │
┌────────▼────────┐    ┌─────────▼────────┐    ┌─────────────────┐
│ AuthTokensModule│ ── │ RefreshTokenRepo │ ── │  Token Storage  │
└─────────────────┘    └──────────────────┘    └─────────────────┘
         │
┌────────▼────────┐
│   JWT Tokens    │
└─────────────────┘

🔧 Quick Start

1. Define Your User Type

interface User {
  id: string;
  email: string;
  password: string;
  name: string;
  identifierType: "email" | "phone";
}

interface CreateUserData {
  identifier: string;
  password: string;
  name: string;
}

2. Implement Your User Repository

import { IUserRepository, BaseCreateUserData } from "@khni/auth";
import { PrismaClient } from "@prisma/client";

export interface User {
  id: string;
  email: string;
  password: string;
  name?: string;
  identifierType: "email" | "phone";
  createdAt: Date;
  updatedAt: Date;
}

export interface CreateUserData extends BaseCreateUserData {
  name?: string;
}

export class UserRepository implements IUserRepository<User, CreateUserData> {
  private prisma: PrismaClient;

  constructor(prisma?: PrismaClient) {
    this.prisma = prisma || new PrismaClient();
  }

  async findByIdentifier({
    identifier,
  }: {
    identifier: string;
  }): Promise<User | null> {
    const user = await this.prisma.user.findUnique({
      where: { email: identifier },
    });
    return user ? this.mapPrismaUserToUser(user) : null;
  }

  async create(data: CreateUserData): Promise<User> {
    const user = await this.prisma.user.create({
      data: {
        email: data.identifier,
        password: data.password,
        name: data.name,
        identifierType: "email",
      },
    });
    return this.mapPrismaUserToUser(user);
  }

  async update({
    data,
    identifier,
  }: {
    data: Partial<User>;
    identifier: string;
  }): Promise<User> {
    const { id, createdAt, ...updateData } = data;
    const user = await this.prisma.user.update({
      where: { email: identifier },
      data: updateData,
    });
    return this.mapPrismaUserToUser(user);
  }

  private mapPrismaUserToUser(prismaUser: any): User {
    return {
      id: prismaUser.id,
      email: prismaUser.email,
      password: prismaUser.password,
      name: prismaUser.name,
      identifierType: prismaUser.identifierType,
      createdAt: prismaUser.createdAt,
      updatedAt: prismaUser.updatedAt,
    };
  }
}

3. Implement Refresh Token Repository

import { IRefreshTokenRepository } from "@khni/auth-tokens";
import { PrismaClient } from "@prisma/client";

export class RefreshTokenRepository implements IRefreshTokenRepository {
  private prisma: PrismaClient;

  constructor(prisma?: PrismaClient) {
    this.prisma = prisma || new PrismaClient();
  }

  async create(token: {
    id: string;
    userId: string;
    expiresAt: Date;
  }): Promise<void> {
    await this.prisma.refreshToken.create({
      data: {
        id: token.id,
        userId: token.userId,
        expiresAt: token.expiresAt,
      },
    });
  }

  async findById(
    id: string
  ): Promise<{ id: string; userId: string; expiresAt: Date } | null> {
    const token = await this.prisma.refreshToken.findUnique({
      where: { id },
    });
    return token || null;
  }

  async deleteById(id: string): Promise<void> {
    await this.prisma.refreshToken.delete({
      where: { id },
    });
  }

  async deleteByUserId(userId: string): Promise<void> {
    await this.prisma.refreshToken.deleteMany({
      where: { userId },
    });
  }
}

4. Set Up Complete Authentication System

import { LocalAuthService, BcryptHasher } from "@khni/auth";
import {
  initAuthTokensModule,
  getAuthTokensService,
  type AuthModuleConfig,
} from "@khni/auth-tokens";

// Setup repositories
const userRepository = new UserRepository();
const refreshTokenRepository = new RefreshTokenRepository();

// Initialize authentication service
const authService = new LocalAuthService(userRepository);

// Initialize token module
const authConfig: AuthModuleConfig = {
  jwtSecret: process.env.JWT_SECRET!,
  accessTokenExpiresIn: "15m",
  refreshTokenExpiresIn: "7d",
  refreshTokenRepository,
  findUniqueUserById: async (userId: string) => {
    return await userRepository.findByIdentifier({ identifier: userId });
  },
  logger: console, // optional
};

initAuthTokensModule(authConfig);
const tokensService = getAuthTokensService();

🔐 Complete Authentication Flow

User Registration

async function registerUser(email: string, password: string, name: string) {
  try {
    // 1. Create user in database with hashed password
    const user = await authService.createUser({
      data: {
        identifier: email,
        password: password,
        name: name,
      },
    });

    // 2. Generate access and refresh tokens
    const tokens = await tokensService.generate(user.id);

    // 3. Return user and tokens (exclude password from response)
    const { password: _, ...userWithoutPassword } = user;
    return {
      user: userWithoutPassword,
      tokens,
    };
  } catch (error) {
    if (error.code === "AUTH_USED_IDENTIFIER") {
      throw new Error("User with this email already exists");
    }
    throw new Error("Registration failed");
  }
}

// Usage
const result = await registerUser(
  "[email protected]",
  "securePassword123",
  "John Doe"
);
console.log(result);
// {
//   user: { id: "123", email: "[email protected]", name: "John Doe", ... },
//   tokens: { accessToken: "eyJ...", refreshToken: "abc123..." }
// }

User Login

async function loginUser(email: string, password: string) {
  try {
    // 1. Verify user credentials
    const user = await authService.verifyPassword({
      data: {
        identifier: email,
        password: password,
      },
    });

    // 2. Generate new tokens
    const tokens = await tokensService.generate(user.id);

    // 3. Return user and tokens (exclude password from response)
    const { password: _, ...userWithoutPassword } = user;
    return {
      user: userWithoutPassword,
      tokens,
    };
  } catch (error) {
    if (error.code === "INCORRECT_CREDENTIALS") {
      throw new Error("Invalid email or password");
    }
    throw new Error("Login failed");
  }
}

// Usage
const result = await loginUser("[email protected]", "securePassword123");

Token Refresh

async function refreshTokens(refreshToken: string) {
  try {
    // 1. Verify refresh token and generate new tokens
    const tokens = await tokensService.refresh(refreshToken);

    return {
      tokens,
    };
  } catch (error) {
    if (error.code === "REFRESH_TOKEN_INVALID") {
      throw new Error("Invalid or expired refresh token");
    }
    throw new Error("Token refresh failed");
  }
}

// Usage when access token expires
const newTokens = await refreshTokens(oldRefreshToken);

User Logout

async function logoutUser(refreshToken: string) {
  try {
    // 1. Revoke the refresh token
    await tokensService.logout(refreshToken);

    return { success: true };
  } catch (error) {
    throw new Error("Logout failed");
  }
}

// Usage
await logoutUser(refreshToken);

Complete Express.js Example

import express from "express";
import { LocalAuthService, BcryptHasher } from "@khni/auth";
import { initAuthTokensModule, getAuthTokensService } from "@khni/auth-tokens";
import { UserRepository } from "./repositories/UserRepository";
import { RefreshTokenRepository } from "./repositories/RefreshTokenRepository";

const app = express();
app.use(express.json());

// Setup
const userRepository = new UserRepository();
const refreshTokenRepository = new RefreshTokenRepository();
const authService = new LocalAuthService(userRepository);

initAuthTokensModule({
  jwtSecret: process.env.JWT_SECRET!,
  accessTokenExpiresIn: "15m",
  refreshTokenExpiresIn: "7d",
  refreshTokenRepository,
  findUniqueUserById: async (userId) =>
    await userRepository.findByIdentifier({ identifier: userId }),
});

const tokensService = getAuthTokensService();

// Auth middleware
const authenticateToken = (
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) => {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ error: "Access token required" });
  }

  try {
    const accessTokenService = getAccessTokenService();
    const payload = accessTokenService.verify(token);
    req.user = payload; // { userId: "123" }
    next();
  } catch (error) {
    return res.status(403).json({ error: "Invalid or expired token" });
  }
};

// Routes
app.post("/register", async (req, res) => {
  try {
    const { email, password, name } = req.body;
    const user = await authService.createUser({
      data: { identifier: email, password, name },
    });

    const tokens = await tokensService.generate(user.id);
    const { password: _, ...userWithoutPassword } = user;

    res.status(201).json({
      user: userWithoutPassword,
      tokens,
    });
  } catch (error) {
    if (error.code === "AUTH_USED_IDENTIFIER") {
      return res.status(409).json({ error: "User already exists" });
    }
    res.status(500).json({ error: "Registration failed" });
  }
});

app.post("/login", async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await authService.verifyPassword({
      data: { identifier: email, password },
    });

    const tokens = await tokensService.generate(user.id);
    const { password: _, ...userWithoutPassword } = user;

    res.json({
      user: userWithoutPassword,
      tokens,
    });
  } catch (error) {
    if (error.code === "INCORRECT_CREDENTIALS") {
      return res.status(401).json({ error: "Invalid credentials" });
    }
    res.status(500).json({ error: "Login failed" });
  }
});

app.post("/refresh", async (req, res) => {
  try {
    const { refreshToken } = req.body;
    const tokens = await tokensService.refresh(refreshToken);
    res.json({ tokens });
  } catch (error) {
    res.status(401).json({ error: "Invalid refresh token" });
  }
});

app.post("/logout", async (req, res) => {
  try {
    const { refreshToken } = req.body;
    await tokensService.logout(refreshToken);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: "Logout failed" });
  }
});

app.get("/profile", authenticateToken, async (req, res) => {
  try {
    const user = await userRepository.findByIdentifier({
      identifier: req.user.userId,
    });
    if (!user) {
      return res.status(404).json({ error: "User not found" });
    }
    const { password: _, ...userWithoutPassword } = user;
    res.json({ user: userWithoutPassword });
  } catch (error) {
    res.status(500).json({ error: "Failed to fetch profile" });
  }
});

app.listen(3000, () => {
  console.log("Server running on port 3000");
});

🔄 Token Management

Access Tokens

  • Short-lived (15 minutes by default)
  • JWT-based with user payload
  • Stateless - verified using secret key
  • Used for API authentication

Refresh Tokens

  • Long-lived (7 days by default)
  • Stored in database for revocation
  • Used to obtain new access tokens
  • Automatically revoked on logout

🛡️ Security Features

Password Security

  • Bcrypt hashing with configurable rounds
  • Automatic salt generation
  • Timing-attack resistant comparison

Token Security

  • JWT secret key for signing
  • Short expiration times for access tokens
  • Database-backed refresh tokens for revocation
  • Secure token generation using crypto module

Best Practices

  • Never store passwords in plain text
  • Use HTTPS in production
  • Implement rate limiting on auth endpoints
  • Store refresh tokens securely (httpOnly cookies recommended)
  • Implement token blacklisting for immediate revocation

📖 API Reference

LocalAuthService Methods

createUser

Creates a new user with hashed password.

verifyPassword

Verifies user credentials for login.

resetPassword

Updates user password with new hash.

findUserByIdentifier

Finds user by email/phone identifier.

AuthTokensService Methods

generate

Generates new access and refresh tokens for a user.

refresh

Verifies refresh token and generates new tokens.

logout

Revokes a refresh token.

🧪 Testing Your Implementation

import { describe, it, expect, beforeEach } from "vitest";
import { LocalAuthService } from "@khni/auth";
import { initAuthTokensModule, getAuthTokensService } from "@khni/auth-tokens";

describe("Authentication Flow", () => {
  beforeEach(() => {
    // Reset modules and clear database
  });

  it("should complete full authentication flow", async () => {
    // 1. Register user
    // 2. Login with credentials
    // 3. Access protected route with token
    // 4. Refresh tokens
    // 5. Logout
  });
});

🔧 Configuration

Environment Variables

JWT_SECRET=your-super-secure-jwt-secret-key
DATABASE_URL=your-database-connection-string

Token Expiration Settings

const authConfig = {
  accessTokenExpiresIn: "15m", // 15 minutes
  refreshTokenExpiresIn: "7d", // 7 days
  // Supported units: ms, s, m, h, d
};

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/new-feature
  3. Commit changes: git commit -am 'Add new feature'
  4. Push to branch: git push origin feature/new-feature
  5. Submit a pull request

📄 License

MIT License - see LICENSE file for details