Docs/@gablura/auth-core

@gablura/auth-core

Production-ready authentication core for Express.js backends.

@gablura/auth-core

Production-ready authentication infrastructure for full-stack applications.

@gablura/auth-core is the backend foundation of the Gablura auth system. It provides a complete authentication pipeline — token exchange, session binding, refresh rotation, account lockout, 2FA, and audit logging — as a single, configurable module.

Why it exists

Most developers face the same authentication problems repeatedly:

  • Implementing secure token storage and rotation
  • Preventing session hijacking across devices
  • Handling account lockout after failed attempts
  • Adding 2FA without rewriting the entire auth layer
  • Maintaining audit logs for compliance

@gablura/auth-core solves these problems once, so you can focus on building your application.

Features

  • Dual-token architecture — Access + refresh tokens with RS256 signing
  • Token rotation — Automatic refresh token rotation with reuse detection
  • Session binding — Device fingerprint + IP validation to prevent hijacking
  • Account lockout — Configurable failed attempt thresholds with automatic unlock
  • Two-factor authentication — TOTP-based 2FA with backup codes
  • Audit logging — Structured auth event logging with severity levels
  • Redis-backed — Session management, caching, and revocation via Redis
  • Express middleware — Drop-in middleware factory for Express applications
  • Cache layer — Optional auth result and user profile caching
  • Idempotent exchanges — Duplicate request protection for token exchange

Architecture

Browser
  ↓
NextAuth (HMAC proof)
  ↓
@gablura/auth-core
  ↓
Express Backend
  ↓
Redis + MongoDB

The core module sits between your auth provider (NextAuth, custom JWT, etc.) and your backend. It validates proofs, manages tokens, enforces sessions, and logs everything.

Internal modules

TokenManager        — RS256 JWT signing and verification
TokenRevocation     — Refresh token rotation with atomic Redis operations
SessionManager      — Session tracking with LRU eviction
SessionBinding      — Device fingerprint + IP validation
AccountLockout      — Failed attempt tracking with auto-unlock
AuditLog            — Structured event logging
MiddlewareFactory   — Express middleware composition

Each module is independently configurable and can be extended or replaced.

Installation

Requirements

  • Node.js >= 22
  • Redis >= 6 (for session management and caching)
  • Express 4 or 5 (for middleware integration)

Install

npm install @gablura/auth-core ioredis
pnpm add @gablura/auth-core ioredis
yarn add @gablura/auth-core ioredis

Peer dependencies

PackageVersionRequired
ioredis>= 5Yes
express4 or 5For middleware

RSA key generation

@gablura/auth-core uses RS256 (RSA + SHA-256) for JWT signing. You need to generate an RSA key pair.

# Generate private key
openssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:2048

# Extract public key
openssl rsa -pubout -in private-key.pem -out public-key.pem

Store these securely. Never commit them to version control.

Environment variables

# Required
REDIS_URL=redis://localhost:6379
AUTH_PRIVATE_KEY=path/to/private-key.pem
AUTH_PUBLIC_KEY=path/to/public-key.pem

# Optional
AUTH_SESSION_TTL=1800
AUTH_REFRESH_TTL=2592000
AUTH_LOCKOUT_THRESHOLD=5
AUTH_LOCKOUT_WINDOW=900

Quick Start

1. Create the auth service

import { AuthService } from "@gablura/auth-core";
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

const auth = new AuthService({
  privateKey: process.env.AUTH_PRIVATE_KEY!,
  publicKey: process.env.AUTH_PUBLIC_KEY!,
  redis,
});

2. Set up Express middleware

import express from "express";
import { MiddlewareFactory } from "@gablura/auth-core";

const app = express();
const mw = new MiddlewareFactory(auth);

app.use(mw.verifyToken());

3. Exchange a token

// After NextAuth (or custom auth) issues an HMAC proof
app.post("/api/auth/exchange", async (req, res) => {
  const result = await auth.exchange({
    proof: req.body.proof,
    userId: req.body.userId,
    deviceId: req.body.deviceId,
  });

  if (result.ok) {
    res.json({ accessToken: result.accessToken, refreshToken: result.refreshToken });
  } else {
    res.status(401).json({ error: result.error });
  }
});

4. Verify a token

app.get("/api/protected", mw.verifyToken(), (req, res) => {
  res.json({ message: "Authenticated", userId: req.auth.userId });
});

5. Refresh a token

app.post("/api/auth/refresh", async (req, res) => {
  const result = await auth.refresh({
    refreshToken: req.body.refreshToken,
    deviceId: req.body.deviceId,
  });

  if (result.ok) {
    res.json({ accessToken: result.accessToken, refreshToken: result.refreshToken });
  } else {
    res.status(401).json({ error: result.error });
  }
});

That's the basic flow. See Configuration for all options and Usage for advanced patterns.

Configuration

AuthCoreConfig

The AuthService constructor accepts a configuration object:

const auth = new AuthService({
  privateKey: "path/to/private-key.pem",
  publicKey: "path/to/public-key.pem",
  redis,
  // ... optional config
});

Required options

OptionTypeDescription
privateKeystringPath to RSA private key (PEM format)
publicKeystringPath to RSA public key (PEM format)
redisRedisioredis instance for session/cache storage

Optional options

OptionTypeDefaultDescription
sessionTTLnumber1800Access token lifetime in seconds (30 min)
refreshTTLnumber2592000Refresh token lifetime in seconds (30 days)
lockoutThresholdnumber5Failed attempts before lockout
lockoutWindownumber900Lockout duration in seconds (15 min)
cacheEnabledbooleanfalseEnable auth result caching
cacheAuthTTLnumber300Auth cache TTL in seconds (5 min)
cacheUserTTLnumber1800User profile cache TTL in seconds (30 min)
maxConcurrentSessionsnumber10Max sessions per user
keyPrefixstringauth:Redis key prefix
serverSecretstring—Secret for server-to-server binding bypass
trustedProxiesstring[][]CIDR ranges for X-Forwarded-For validation

Defaults

const DEFAULTS = {
  sessionTTL: 1800,        // 30 minutes
  refreshTTL: 2592000,     // 30 days
  lockoutThreshold: 5,
  lockoutWindow: 900,      // 15 minutes
  cacheEnabled: false,
  cacheAuthTTL: 300,       // 5 minutes
  cacheUserTTL: 1800,      // 30 minutes
  maxConcurrentSessions: 10,
  keyPrefix: "auth:",
};

Resolving configuration

Use resolveConfig to merge user options with defaults:

import { resolveConfig } from "@gablura/auth-core";

const config = resolveConfig({
  privateKey: process.env.AUTH_PRIVATE_KEY!,
  publicKey: process.env.AUTH_PUBLIC_KEY!,
  redis,
  lockoutThreshold: 10, // override default
});

Usage

API hierarchy

@gablura/auth-core organizes its API into three tiers:

Primary API

Daily-use operations:

auth.exchange({ proof, userId, deviceId })    // Exchange HMAC proof for tokens
auth.verify({ token, deviceId })              // Verify access token
auth.refresh({ refreshToken, deviceId })      // Rotate refresh token
auth.logout({ refreshToken })                 // Revoke refresh token
auth.enable2FA({ userId })                    // Generate 2FA secret
auth.verify2FA({ userId, code })              // Verify TOTP code
auth.getSessions({ userId })                  // List active sessions
auth.revokeSession({ sessionId })             // Revoke a single session
auth.revokeAllSessions({ userId })            // Revoke all sessions for user

Extension API

Additional utilities:

auth.recordLoginFailure({ userId })           // Record failed login attempt
auth.resetLoginFailures({ userId })           // Clear failed login attempts
auth.checkLockout({ userId })                 // Check if account is locked
auth.getAuditLog({ userId, limit })           // Retrieve audit events

Advanced API

Internal access when you need it:

auth.getTokenManager()                        // Direct TokenManager access
auth.getTokenRevocation()                     // Direct TokenRevocation access
auth.getSessionManager()                      // Direct SessionManager access
auth.getAccountLockout()                      // Direct AccountLockout access
auth.getAuditLog()                            // Direct AuditLog access
auth.getRedis()                               // Direct Redis access
auth.getConfig()                              // Current resolved config
auth.invalidateUserCache({ userId })          // Clear user cache

Express middleware

import { MiddlewareFactory } from "@gablura/auth-core";

const mw = new MiddlewareFactory(auth);

// Require valid token
app.use(mw.verifyToken());

// Optional token (sets req.auth if valid, continues if not)
app.use(mw.optionalToken());

// Require CSRF validation
app.use(mw.verifyCsrf());

// Rate limiting
app.use(mw.rateLimit({ windowMs: 60000, max: 100 }));

// Session timeout
app.use(mw.sessionTimeout());

Adapters

Implement these interfaces to connect to your data store:

UserStore

interface UserStore {
  findUserById(id: string): Promise<User | null>;
  findUserByEmail(email: string): Promise<User | null>;
}

CacheAdapter

interface CacheAdapter {
  get(key: string): Promise<string | null>;
  set(key: string, value: string, ttl?: number): Promise<void>;
  del(key: string): Promise<void>;
}

RedisAdapter

The built-in Redis adapter handles all session and cache operations. Use it unless you need a custom implementation.

API Reference

AuthService

exchange(params)

Exchange an HMAC proof for token pair.

auth.exchange({
  proof: string,        // HMAC proof from auth provider
  userId: string,       // User identifier
  deviceId: string,     // Device identifier
}): Promise<ExchangeResult>

ExchangeResult:

{ ok: true, accessToken: string, refreshToken: string }
| { ok: false, error: string }

verify(params)

Verify an access token.

auth.verify({
  token: string,        // Access token JWT
  deviceId: string,     // Expected device ID
}): Promise<VerifyResult>

VerifyResult:

{ ok: true, userId: string, sessionId: string }
| { ok: false, error: string }

refresh(params)

Rotate a refresh token and issue new pair.

auth.refresh({
  refreshToken: string,  // Current refresh token
  deviceId: string,     // Device identifier
}): Promise<RefreshResult>

logout(params)

Revoke a refresh token.

auth.logout({
  refreshToken: string,
}): Promise<{ ok: boolean }>

enable2FA(params)

Generate a TOTP secret for 2FA enrollment.

auth.enable2FA({
  userId: string,
}): Promise<{ secret: string, backupCodes: string[] }>

verify2FA(params)

Verify a TOTP code.

auth.verify2FA({
  userId: string,
  code: string,         // 6-digit TOTP code
}): Promise<{ ok: boolean }>

getSessions(params)

List active sessions for a user.

auth.getSessions({
  userId: string,
}): Promise<Session[]>

revokeSession(params)

Revoke a single session.

auth.revokeSession({
  sessionId: string,
}): Promise<{ ok: boolean }>

revokeAllSessions(params)

Revoke all sessions for a user.

auth.revokeAllSessions({
  userId: string,
}): Promise<{ ok: boolean }>

Error classes

ClassStatusError Code
AuthError401AUTH_ERROR
TokenExpiredError401TOKEN_EXPIRED
TokenRevokedError401TOKEN_REVOKED
SessionBindingError401SESSION_BINDING_FAILED
AccountLockedError423ACCOUNT_LOCKED
InvalidProofError401INVALID_PROOF
RateLimitError429RATE_LIMIT_EXCEEDED

Examples

Express integration

import express from "express";
import { AuthService, MiddlewareFactory } from "@gablura/auth-core";
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);
const auth = new AuthService({
  privateKey: process.env.AUTH_PRIVATE_KEY!,
  publicKey: process.env.AUTH_PUBLIC_KEY!,
  redis,
  cacheEnabled: true,
});

const app = express();
const mw = new MiddlewareFactory(auth);

app.use(express.json());

// Protected route
app.get("/api/profile", mw.verifyToken(), (req, res) => {
  res.json({ userId: req.auth.userId, sessionId: req.auth.sessionId });
});

// Token exchange
app.post("/api/auth/exchange", async (req, res) => {
  try {
    const result = await auth.exchange({
      proof: req.body.proof,
      userId: req.body.userId,
      deviceId: req.body.deviceId,
    });

    if (result.ok) {
      res.json({
        accessToken: result.accessToken,
        refreshToken: result.refreshToken,
      });
    } else {
      res.status(401).json({ error: result.error });
    }
  } catch (err) {
    res.status(500).json({ error: "Internal server error" });
  }
});

// Refresh
app.post("/api/auth/refresh", async (req, res) => {
  const result = await auth.refresh({
    refreshToken: req.body.refreshToken,
    deviceId: req.body.deviceId,
  });

  if (result.ok) {
    res.json({
      accessToken: result.accessToken,
      refreshToken: result.refreshToken,
    });
  } else {
    res.status(401).json({ error: result.error });
  }
});

Error handling

import { AuthError, TokenExpiredError, AccountLockedError } from "@gablura/auth-core";

try {
  const result = await auth.verify({ token, deviceId });
  if (!result.ok) {
    // Handle specific errors
    if (result.error === "TOKEN_EXPIRED") {
      // Prompt refresh
    }
  }
} catch (err) {
  if (err instanceof AccountLockedError) {
    // Show lockout message
  } else if (err instanceof AuthError) {
    // Generic auth error
  }
}

Custom error factory

const auth = new AuthService({
  privateKey: process.env.AUTH_PRIVATE_KEY!,
  publicKey: process.env.AUTH_PUBLIC_KEY!,
  redis,
  errorFactory: (code, message) => {
    // Custom error formatting
    return new AppError(code, message);
  },
});

Security

Token architecture

  • RS256 signing — RSA + SHA-256 for JWT integrity
  • Token rotation — Refresh tokens are single-use; each refresh issues a new pair
  • Reuse detection — If a revoked refresh token is presented, all sessions for that user are revoked
  • Idempotent exchanges — Duplicate token exchange requests return the same tokens

Session binding

Sessions are bound to a device fingerprint + IP address:

  • Device fingerprint — Derived from User-Agent headers with normalization
  • IP validation — Compares request IP against session IP
  • Private IP detection — Handles local/private network addresses correctly
  • Trusted proxies — CIDR-aware X-Forwarded-For validation for reverse proxy setups
const auth = new AuthService({
  // ...
  trustedProxies: ["10.0.0.0/8", "172.16.0.0/12"],
});

Server-to-server binding

For backend-to-backend calls where device fingerprints aren't available:

const auth = new AuthService({
  // ...
  serverSecret: process.env.AUTH_SERVER_SECRET,
});

When serverSecret is set, requests with a valid HMAC proof from the server secret bypass device fingerprint validation.

Account lockout

Configurable brute-force protection:

const auth = new AuthService({
  // ...
  lockoutThreshold: 5,    // Lock after 5 failures
  lockoutWindow: 900,     // Lock for 15 minutes
});

CSRF protection

app.use(mw.verifyCsrf());

Validates CSRF tokens using timing-safe comparison.

Rate limiting

app.use(mw.rateLimit({
  windowMs: 60000,   // 1 minute window
  max: 100,          // 100 requests per window
}));

Timing-safe comparisons

All HMAC and CSRF token comparisons use crypto.timingSafeEqual to prevent timing attacks.

Performance

Caching

When cacheEnabled: true, the module caches:

DataTTLPurpose
Auth results5 minAvoid repeated token verification
User profiles30 minReduce database lookups
Refresh dedup30 secPrevent concurrent refresh races

Redis operations

  • Atomic Lua scripts — Token rotation uses a single Lua script for atomicity
  • Distributed locks — Prevents concurrent refresh token rotations
  • Pipeline operations — Lockout tracking uses Redis pipelines
  • LRU eviction — Max concurrent sessions enforced with least-recently-used eviction

Tuning

Increase cache TTL

For high-traffic applications:

const auth = new AuthService({
  // ...
  cacheEnabled: true,
  cacheAuthTTL: 600,    // 10 minutes
  cacheUserTTL: 3600,   // 1 hour
});

Adjust session limits

const auth = new AuthService({
  // ...
  maxConcurrentSessions: 20,  // Allow more concurrent sessions
});

Redis memory

Each session uses approximately 200-500 bytes in Redis. For 10,000 concurrent sessions, plan for ~5MB of Redis memory for auth data.

FAQ

Can I use this without NextAuth?

Yes. @gablura/auth-core is backend-only. You can use any auth provider that can issue an HMAC proof, or build your own.

Can I use this without Express?

Yes. The AuthService class is framework-agnostic. You can use it with Fastify, Hono, or any Node.js HTTP server. The MiddlewareFactory is Express-specific, but the core service works everywhere.

What Redis versions are supported?

Redis 6 and above. The module uses features like Lua scripting and pipelines that are available in all modern Redis versions.

Can I use this with App Router?

Yes. @gablura/auth-next provides App Router support. @gablura/auth-core works with any Next.js routing model.

How do I rotate RSA keys?

Generate new keys and update the environment variables. Existing tokens signed with the old key will remain valid until they expire. New tokens will use the new key.

What happens if Redis goes down?

The behavior depends on your configuration:

  • Without cache: Auth still works, but session management and revocation stop working
  • With cache: Cached results continue to work, but new verifications hit the database

How do I handle deployment?

During deployment, existing sessions remain valid as long as Redis is persistent. If you restart Redis, all sessions are lost and users must re-authenticate.

Can I use this with MongoDB?

Yes. Implement the UserStore interface to connect to MongoDB. The module doesn't assume any specific database.

Changelog

1.2.4

  • Fix refresh deduplication race condition
  • Add serverSecret for server-to-server binding bypass
  • Add trustedProxies for CIDR-aware IP validation

1.2.3

  • Add maxConcurrentSessions with LRU eviction
  • Add keyPrefix configuration
  • Improve audit log structured output

1.2.2

  • Add idempotency keys for token exchange
  • Add refresh deduplication with 30-second TTL
  • Fix session binding for private IP addresses

1.2.1

  • Add cacheEnabled option
  • Add user profile caching with 30-minute TTL
  • Add auth result caching with 5-minute TTL

1.2.0

  • Add 2FA support (TOTP + backup codes)
  • Add session management API
  • Add account lockout
  • Add audit logging

1.1.0

  • Add session binding (device fingerprint + IP)
  • Add CSRF protection
  • Add rate limiting middleware

1.0.0

  • Initial release
  • Dual-token architecture (access + refresh)
  • RS256 JWT signing
  • Token rotation with reuse detection
  • Express middleware factory
  • Redis adapter

Contributing

Development setup

git clone https://github.com/gablura/auth.git
cd auth
pnpm install

Project structure

packages/
├── core/           # @gablura/auth-core
│   ├── src/
│   └── package.json
└── next/           # @gablura/auth-next
    ├── src/
    └── package.json

Scripts

pnpm test          # Run tests
pnpm build         # Build all packages
pnpm lint          # Lint all packages

Pull requests

  1. Create a feature branch from main
  2. Make your changes
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

Code style

  • TypeScript strict mode
  • ESLint with the project config
  • Prettier for formatting

Testing

pnpm test          # Run all tests
pnpm test:watch    # Watch mode

License

By contributing, you agree that your contributions will be licensed under the MIT License.

License

MIT License

Copyright 2026 Gablura

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.