Getting Started
@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:
- Node.js 20 or later
- Docker
Local/Library:
- Node.js 20 or later
- MySQL 8+
mmdbctlbinary for MMDB compilation (installed automatically during build)
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:
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
age-keygen -o age_key && age-keygen -y age_key > public_key
This will generate 2 files:
age_keyis the private key. Store it securely.public_keyis the public key.
age_key to version controlEncrypt your configuration
Create your config.json with the full configuration object, then encrypt it:
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.
config.json from the local host and storing age_key in an appropriate secret manager.Create a 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/serverfor the auth logs, can be any local path../auth-logs/server/bot-detectorfor thebot-detectorlogs, can be any local path.bot-detector-datafor keeping the data sources fresh, and compilingbot-detector generatedata sources. Can be any name.email-datafor the disposable email list data. Can be any name.
Start the service
docker compose up -d
Image security hardening
The production image enforces the following:
| Control | Value |
|---|---|
| Non-root user | appuser (UID 10001) |
| Filesystem | read_only: true |
| Capabilities | All dropped (cap_drop: ["ALL"]) |
| Privilege escalation | Blocked (no-new-privileges: true) |
| PID limit | 200 |
| Config lifetime | Decrypted into tmpfs, deleted after load |
| Healthcheck | GET /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
yarn add @riavzon/auth
npm install @riavzon/auth
bun add @riavzon/auth
The module also requires express, cookie-parser, and mysql2 as peer dependencies:
pnpm add express cookie-parser mysql2
yarn add express cookie-parser mysql2
npm install express cookie-parser mysql2
bun 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.
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
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]"
yarn bot-detector init --contact="[email protected]"
npx bot-detector init --contact="[email protected]"
bunx 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:
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:
- Downloads and compiles the disposable email domain blocklist into an LMDB database at
dist/email-db/disposable-emails.mdb. - Creates all required auth MySQL tables (
users,refresh_tokens,mfa_codes, etc.). - Creates the Bot Detector tables used for IP analysis and threat scoring.
You can also call it in your code:
import { initAuthData } from '@riavzon/auth'
await initAuthData(config)
auth binary is available after installing the package. If running locally without a global install, use npx @riavzon/auth ./config.json instead.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:
import { bootstrapApp } from '@riavzon/auth/service'
const app = await bootstrapApp(config)
app.listen(10000, '0.0.0.0')
The middleware chain bootstrapApp applies:
- Health check endpoint (
GET /health) - Bot Detector configuration and table creation
- Auth table creation
- HTTP logger (Pino)
- Disable
x-powered-by - Helmet security headers
- Service headers
- IP validation
- HMAC authentication (when
service.Hmacis configured) - Public API token verification route
express.json()cookie-parser- Bot Detector
ApiResponsemiddleware - Authentication routes
- Token rotation routes
- Magic link routes
- BFF access route
- API token management routes
- Operational config route
- Bot Detector warm-up
- 404 handler
- Global error handler
Environment variables
The standalone service reads the configuration from a JSON file. Override the path with:
| Variable | Default | Description |
|---|---|---|
CONFIG_PATH | /run/app/config.json | Path to the JSON configuration file |
SKIP_CONFIG_UNLINK | false | Set to true to keep the config file in the container after loading |
NODE_ENV | - | Set to production in the Docker image |