@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.
Maintainers
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-stringToken Expiration Settings
const authConfig = {
accessTokenExpiresIn: "15m", // 15 minutes
refreshTokenExpiresIn: "7d", // 7 days
// Supported units: ms, s, m, h, d
};🤝 Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feature/new-feature - Commit changes:
git commit -am 'Add new feature' - Push to branch:
git push origin feature/new-feature - Submit a pull request
📄 License
MIT License - see LICENSE file for details
