Getting Started

Prerequisites, installation, Docker deployment, and first run of the IAM service.

@riavzon/auth runs as a standalone Express 5 service backed by MySQL. You can use it as a library in your own application, or run the production-ready Docker image that ships with secrets encrypted at rest.

Requirements

Depending on how you plan to use the service, your environment needs to meet the following:

Docker with encryption:

Docker without encryption:

Local/Library:

  • Node.js 20 or later
  • MySQL 8+
  • mmdbctl binary for MMDB compilation (installed automatically during build)
The recommended deployment path is the hardened Docker image. It handles all dependencies, database initialization, mmdbctl installations, and data-source compilation automatically.

Docker deployment

The IAM service ships as a public Docker image. The image uses a multi-stage build: it installs all required binaries (age, mmdbctl), dependencies, compiles all Shield Base and Bot Detector data sources, bundles the service, then produces a minimal image with read_only filesystem support.

Secrets at rest

The Docker image uses age to encrypt and decrypt your configuration file (config.json). Encrypt your config locally with age, and the image decrypts it at startup using the decrypt.sh entrypoint script. The service deletes the decrypted file automatically after loading it.

You can provide the age key and encrypted config as Docker secrets.

If you don't want to deal with encryption, mount your config file directly at /run/app/config.json:

docker-compose.yml
  auth:
    image: sergio68/auth
    read_only: true  
    restart: unless-stopped
    cap_drop: ["ALL"]
    user: 10001:10001
    volumes: 
      - ./auth-logs/server:/app/auth-logs:rw
      - ./auth-logs/server/bot-detector:/app/bot-detector-logs:rw
      - ./config.json:/run/app/config.json
      - bot-detector-data:/app/node_modules/@riavzon/bot-detector/dist/_data-sources:rw
      - email-data:/app/dist/email-db:rw
    tmpfs:
      - /run/app:rw,noexec,nosuid,nodev,uid=10001,gid=10001,size=1m
    pids_limit: 200
    ports:
      - "10000:10000"
    security_opt:
      - "no-new-privileges:true"
    depends_on:
      mysql:
        condition: service_healthy

Generate an age key pair

Terminal
age-keygen -o age_key && age-keygen -y age_key > public_key

This will generate 2 files:

  • age_key is the private key. Store it securely.
  • public_key is the public key.
Never commit age_key to version control

Encrypt your configuration

Create your config.json with the full configuration object, then encrypt it:

Terminal
age -a -e -r "$(cat public_key)" -o config.json.age config.json

config.json.age is your encrypted config. Only the matching private key can decrypt this file.

In production, consider deleting config.json from the local host and storing age_key in an appropriate secret manager.

Create a docker-compose.yml

docker-compose.yml
services:
  mysql: 
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: secure_password
      MYSQL_DATABASE: auth_db
      MYSQL_USER: auth_user
      MYSQL_PASSWORD: secure_password
    cap_drop: ["ALL"]
    user: "999:999"
    security_opt: 
      - "no-new-privileges:true"
    volumes:
      - sql_db:/var/lib/mysql
    healthcheck:
      test: ["CMD-SHELL", "bash -lc 'exec 3<>/dev/tcp/127.0.0.1/3306'"]
      interval: 10s
      timeout: 8s 
      retries: 5
      start_period: 7m
    tmpfs:
      - /var/lib/mysql:rw,noexec,nosuid,nodev,size=512m  

  auth:
    image: sergio68/auth
    read_only: true  
    restart: unless-stopped
    cap_drop: ["ALL"]
    user: 10001:10001
    volumes: 
      - ./auth-logs/server:/app/auth-logs:rw
      - ./auth-logs/server/bot-detector:/app/bot-detector-logs:rw
      - bot-detector-data:/app/node_modules/@riavzon/bot-detector/dist/_data-sources:rw
      - email-data:/app/dist/email-db:rw
    tmpfs:
      - /run/app:rw,noexec,nosuid,nodev,uid=10001,gid=10001,size=1m
    pids_limit: 200
    ports:
      - "10000:10000"
    secrets:
      - age_key
      - encrypted_config
    security_opt:
      - "no-new-privileges:true"
    depends_on:
      mysql:
        condition: service_healthy

volumes:
  sql_db:  
  bot-detector-data:
  email-data:

secrets:
  age_key:
    file: ./age_key
  encrypted_config:
    file: ./config.json.age

The volumes that are required are:

  • ./auth-logs/server for the auth logs, can be any local path.
  • ./auth-logs/server/bot-detector for the bot-detector logs, can be any local path.
  • bot-detector-data for keeping the data sources fresh, and compiling bot-detector generate data sources. Can be any name.
  • email-data for the disposable email list data. Can be any name.

Start the service

Terminal
docker compose up -d

Image security hardening

The production image enforces the following:

ControlValue
Non-root userappuser (UID 10001)
Filesystemread_only: true
CapabilitiesAll dropped (cap_drop: ["ALL"])
Privilege escalationBlocked (no-new-privileges: true)
PID limit200
Config lifetimeDecrypted into tmpfs, deleted after load
HealthcheckGET /health every 30s

Library installation

If you prefer to integrate the IAM module into your own Express application rather than running the standalone service, install it as a dependency.

pnpm add @riavzon/auth

The module also requires express, cookie-parser, and mysql2 as peer dependencies:

pnpm add express cookie-parser mysql2

Quick start (library mode)

Configure the service

Call configuration() once at startup. The only required fields are store, password, jwt, email, and magic_links. See the full Configuration reference for every option.

config.ts
export const config = {
  store: {
    main: { 
      host: 'localhost',
      user: 'root',
      password: 'secret',
      database: 'auth' 
    },
    rate_limiters_pool: {
      store: { 
        host: 'localhost',
        user: 'root',
        password: 'secret',
        database: 'auth_limiters' 
      },
      dbName: 'auth_limiters',
    },
  },
  password: { 
    pepper: process.env.PEPPER! 
  },
  botDetector: { 
    enableBotDetector: true 
  },
  htmlSanitizer: { 
    IrritationCount: 50, 
    maxAllowedInputLength: 50000 
  },
  magic_links: {
    jwt_secret_key: process.env.MAGIC_SECRET!,
    domain: 'https://example.com',
    notificationEmail: {
      privacyPolicyLink: 'https://example.com/privacy',
      contactPageLink: 'https://example.com/contact',
      changePasswordPageLink: 'https://example.com/settings/password',
      loginPageLink: 'https://example.com/login',
    },
  },
  jwt: {
    jwt_secret_key: process.env.JWT_SECRET!,
    access_tokens: {},
    refresh_tokens: {
      refresh_ttl: 604800000,
      domain: 'example.com',
      MAX_SESSION_LIFE: 2592000000,
      maxAllowedSessionsPerUser: 5,
      byPassAnomaliesFor: 300000,
    },
  },
  email: { resend_key: process.env.RESEND_KEY!, email: '[email protected]' },
}

Mount the routes

server.ts
import express from 'express'
import cookieParser from 'cookie-parser'
import {
  authenticationRoutes,
  magicLinks,
  tokenRotationRoutes,
  bffAccessRoute,
} from '@riavzon/auth'
import { configuration } from '@riavzon/auth'
import { config } from './config.js';

const app = express()
app.use(cookieParser())

await configuration(config)
app.use(authenticationRoutes)
app.use(tokenRotationRoutes)
app.use(magicLinks)
app.use(bffAccessRoute)

app.listen(10000)

Compile the Bot Detector data sources

The auth service depends on Bot Detector for IP analysis, threat scoring, and bot detection. Download and compile all required data sources before starting the service:

pnpm bot-detector init --contact="[email protected]"

The --contact flag provides a User-Agent string for BGP data downloads. This step compiles MMDB and LMDB databases for geolocation, ASN, proxy detection, threat lists, and more. See the Bot Detector documentation for the full list of data sources.

Create the database tables

Run the auth CLI to compile the disposable email database and create the required database tables. Pass the path to your config.json as an argument:

Terminal
auth ./config.json

The CLI accepts the config path as a positional argument, or reads it from the CONFIG_PATH environment variable. If neither is provided, it defaults to ./config.json.

This command runs three tasks sequentially:

  1. Downloads and compiles the disposable email domain blocklist into an LMDB database at dist/email-db/disposable-emails.mdb.
  2. Creates all required auth MySQL tables (users, refresh_tokens, mfa_codes, etc.).
  3. Creates the Bot Detector tables used for IP analysis and threat scoring.

You can also call it in your code:

main.ts
import { initAuthData } from '@riavzon/auth'
await initAuthData(config)
The auth binary is available after installing the package. If running locally without a global install, use npx @riavzon/auth ./config.json instead.
Call configuration() before mounting any routes or using any exported function. The entire module reads the resolved config at runtime; calling an export before configuration throws immediately.

Using bootstrapApp

For the fastest path, the module exports bootstrapApp, which wires up the full middleware chain (Helmet, IP validation, the public API verification route, HMAC, cookie parser, Bot Detector, the authenticated routers, and error handling) in the correct order and returns a ready-to-use Express app:

server.ts
import { bootstrapApp } from '@riavzon/auth/service'

const app = await bootstrapApp(config)
app.listen(10000, '0.0.0.0')

The middleware chain bootstrapApp applies:

  1. Health check endpoint (GET /health)
  2. Bot Detector configuration and table creation
  3. Auth table creation
  4. HTTP logger (Pino)
  5. Disable x-powered-by
  6. Helmet security headers
  7. Service headers
  8. IP validation
  9. HMAC authentication (when service.Hmac is configured)
  10. Public API token verification route
  11. express.json()
  12. cookie-parser
  13. Bot Detector ApiResponse middleware
  14. Authentication routes
  15. Token rotation routes
  16. Magic link routes
  17. BFF access route
  18. API token management routes
  19. Operational config route
  20. Bot Detector warm-up
  21. 404 handler
  22. Global error handler
In library mode, you do not have to register any particular route. You can use the exported methods directly to compose your own flows.

Environment variables

The standalone service reads the configuration from a JSON file. Override the path with:

VariableDefaultDescription
CONFIG_PATH/run/app/config.jsonPath to the JSON configuration file
SKIP_CONFIG_UNLINKfalseSet to true to keep the config file in the container after loading
NODE_ENV-Set to production in the Docker image
Logo