Skip to content

Repository files navigation

AuthPool

npm version CI License: MIT npm downloads

A plug-and-play Node.js authentication server. One function call gives you Google OAuth, email/password login, JWT access tokens, rotating refresh tokens with theft detection, CSRF protection, rate limiting, brute-force lockout, and role-based access control — all production-ready.


Install

npm install authpool

Quickstart

const { startAuthServer } = require("authpool");

startAuthServer({
  mongoURI:      process.env.MONGO_URI,
  jwtSecret:     process.env.JWT_SECRET,
  sessionSecret: process.env.SESSION_SECRET,
});

The server starts at http://localhost:5000. All auth routes are live immediately.


Environment Variables

Create a .env file in your project root. AuthPool loads it automatically — you don't need to call require('dotenv') yourself.

Variable Required Description
MONGO_URI MongoDB connection string
JWT_SECRET JWT signing secret (16+ characters, 32+ recommended)
SESSION_SECRET Session cookie secret (different from JWT)
GOOGLE_CLIENT_ID OAuth Google OAuth client ID
GOOGLE_CLIENT_SECRET OAuth Google OAuth client secret
GOOGLE_CALLBACK_URL OAuth e.g. http://localhost:5000/auth/google/callback
PORT Listening port (default: 5000)
CSRF_SECRET Separate CSRF secret (defaults to SESSION_SECRET)
REDIS_URL Redis connection URL
LOG_LEVEL Pino log level (default: debug in dev, info in prod)

Config is validated with Zod on startup. Missing or invalid values throw a single AuthPoolConfigError listing every problem at once — the process is never killed by the library itself; your app decides what to do with the error.

startAuthServer({ /* ... */ }).catch((err) => {
  console.error("Failed to start AuthPool:", err.message);
  process.exit(1);
});

API Routes

Health & status

Method Path Description
GET / Returns { status, package, version }
GET /health Checks MongoDB (hard dependency), Redis (soft dependency), and uptime. Returns 200 (healthy), 200 with "degraded" (Redis down, app still works), or 503 (Mongo down)

Auth

Method Path Auth required Description
GET /auth/csrf Returns a CSRF token (also sent in response header)
POST /auth/register Create account with { email, password, name }
POST /auth/login Login with { email, password }
GET /auth/google Redirect to Google OAuth consent screen
GET /auth/google/callback OAuth callback — handled automatically
GET /auth/protected Bearer JWT Test route — returns decoded token payload
GET /auth/me Bearer JWT Returns full user record from MongoDB
GET /auth/admin Bearer JWT + admin role Admin-only test route
POST /auth/refresh Cookie Rotate refresh token, get new access token
GET /auth/logout Revoke refresh token and clear cookie
POST /auth/logout-all Bearer JWT Invalidate all tokens across all devices, immediately

Register & Login responses

{ "accessToken": "<jwt>", "roles": ["user"] }

A refreshToken httpOnly cookie is set automatically — it's never returned in the response body.


Full Configuration

const { startAuthServer } = require("authpool");

const { app, server } = await startAuthServer({
  // Required (or set via .env)
  mongoURI:      "mongodb://localhost:27017/myapp",
  jwtSecret:     "super-secret-32-char-minimum",
  sessionSecret: "another-secret-32-char-minimum",

  // Google OAuth (optional)
  googleClientID:     "...",
  googleClientSecret: "...",
  googleCallbackURL:  "http://localhost:5000/auth/google/callback",

  // Server
  port: 5000,

  // CORS
  corsOptions: {
    origin:      "http://localhost:3000",
    methods:     ["GET", "POST"],
    credentials: true,
  },

  // Rate limiting (these are the defaults — only override what you need)
  rateLimit: {
    global:   { windowMs: 15 * 60 * 1000, max: 300 },
    auth:     { windowMs: 60 * 1000,       max: 30  },
    slowdown: { windowMs: 60 * 1000, delayAfter: 3, delayMs: 300 },
  },

  // CSRF (enabled by default)
  csrf: {
    enabled:    true,
    headerName: "x-csrf-token",
    cookieName: "authpool.csrf",
    secret:     "optional-separate-csrf-secret",
  },

  // Redis (optional but recommended for production)
  redis: {
    url: "redis://localhost:6379",
    // host: "localhost", port: 6379   ← alternative
    // enabled: false                  ← force in-memory
  },

  // Transform an OAuth profile before the DB upsert (optional)
  transformUser: (profile, provider) => ({
    googleId:   profile.id,
    email:      profile.emails?.[0]?.value,
    name:       profile.displayName,
    profilePic: profile.photos?.[0]?.value,
    roles:      ["user"],
  }),

  // Add your own routes after AuthPool finishes startup
  onReady: (app, server) => {
    app.get("/api/hello", (req, res) => res.json({ message: "custom route" }));
  },
});

Frontend Usage

1 — Register

const res = await fetch("http://localhost:5000/auth/register", {
  method: "POST",
  headers: { "Content-Type": "application/json", "x-csrf-token": csrfToken },
  credentials: "include",
  body: JSON.stringify({ email, password, name }),
});
const { accessToken } = await res.json();

2 — Attach the token to requests

fetch("/api/protected-resource", {
  headers: { Authorization: `Bearer ${accessToken}` },
  credentials: "include",
});

3 — Refresh silently when the token expires

async function refresh() {
  const res = await fetch("http://localhost:5000/auth/refresh", {
    method: "POST",
    credentials: "include",
  });
  const { accessToken } = await res.json();
  return accessToken;
}

4 — CSRF token

const res = await fetch("http://localhost:5000/auth/csrf", { credentials: "include" });
const { csrfToken } = await res.json();

Adding Protected Routes

const verifyJWT          = require("authpool/src/middleware/verifyJWT");
const { authorizeRoles } = require("authpool/src/middleware/authorizeRoles");

startAuthServer({
  onReady: (app) => {
    const JWT_SECRET = process.env.JWT_SECRET;

    app.get("/api/profile", verifyJWT(JWT_SECRET), (req, res) => {
      res.json({ user: req.user });
    });

    app.get("/api/admin", verifyJWT(JWT_SECRET), authorizeRoles(["admin"]), (req, res) => {
      res.json({ message: "admin area" });
    });
  },
});

Security Summary

Feature Default
Password hashing bcrypt, 12 salt rounds
Access token expiry 15 minutes
Refresh token expiry 30 days, rotated on every use
Refresh token storage SHA-256 hashed in MongoDB, httpOnly cookie
Refresh token reuse Detected — revokes entire token family
JWT algorithm Pinned to HS256 (rejects alg confusion)
CSRF protection Double-submit cookie (csrf-csrf)
Brute-force lockout 5 failures → 15-minute IP lockout
Rate limiting Global 300/15 min, credential 30/min
HTTP headers helmet defaults
Session storage MongoDB (connect-mongo), not MemoryStore
Email uniqueness Enforced at DB level (unique index)
Config validation Zod schema, fails fast with a full error list
Logout-all invalidation Immediate (cache purged, no staleness window)

Refresh Token Theft Detection

AuthPool tracks refresh tokens in "families" — every token issued from a single login shares a familyId, and every rotation carries it forward. If a refresh token that was already rotated (and therefore revoked) is presented again, AuthPool treats it as a signal of token theft:

  • The entire family is revoked immediately — every token descended from that login, including ones that haven't been used yet.
  • The client receives a 401 directing them to log in again.
  • A stolen-and-replayed refresh token can only be used once before the legitimate user's next refresh silently invalidates it too.

Observability

  • GET /health — checks MongoDB (hard dependency, returns 503 if down) and Redis (soft dependency — the app degrades to in-memory stores without it), plus process uptime.
  • Structured logging via Pino — every request is logged with a unique request ID (x-request-id, echoed back in the response header), method, path, status code, and response time. Sensitive fields (Authorization, Cookie, passwords, secrets) are automatically redacted.

Redis

Redis is optional. Without it, everything works using in-process memory (single server only). With it, rate limiters, brute-force counters, and the JWT user cache scale across multiple instances and survive restarts.

REDIS_URL=redis://localhost:6379

Pass redis: { enabled: false } to force in-memory mode even when REDIS_URL is set (useful in tests).


Docker (optional)

A Dockerfile and docker-compose.yml are included for local development but are entirely optional — AuthPool runs fine directly with node/npm and doesn't require Docker in any way.

docker compose up --build   # starts app + MongoDB + Redis together
docker compose down -v      # stop and wipe data

Testing

npm test          # Jest unit + integration suite, with coverage
npm run test:legacy  # original manual Postman-style script (tests/test.js)

The suite covers registration, login, CSRF, JWT algorithm pinning, refresh token rotation, reuse/theft detection, and immediate logout-all cache invalidation — run against an ephemeral in-memory MongoDB (mongodb-memory-server), so no real database is needed to run tests.

CI runs this suite on every push and pull request via GitHub Actions.


TypeScript

Types are included at authpool/types/index.d.ts:

import { startAuthServer, AuthPoolOptions } from "authpool";

const options: AuthPoolOptions = { /* ... */ };
await startAuthServer(options);

Requirements

  • Node.js 18+
  • MongoDB 5+ (local or Atlas)
  • Redis (optional, recommended for production)

Roadmap

  • Email verification + password reset
  • Multi-factor authentication (TOTP)
  • Session management (list/revoke active devices)
  • GitHub OAuth
  • Multi-database support (Postgres via Prisma, pluggable repository layer)
  • OpenAPI/Swagger documentation

Security

Found a vulnerability? See SECURITY.md for how to report it responsibly.


Changelog

See CHANGELOG.md for release history and upgrade notes.


License

MIT

About

No description, website, or topics provided.

Resources

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages