Authentication & Security¶
This document describes how Noctuary's authentication and session system works, how to configure it, and what system administrators need to know to operate it safely.
Architecture overview¶
Noctuary uses Ory Kratos as its identity backend — a self-hosted, open-source identity server that handles credentials, email verification, and account recovery. The Noctuary web application communicates with Kratos via server-side API flows: users never interact with Kratos directly, and Kratos UI pages are never exposed.
Browser Noctuary web app Ory Kratos
│ (Go / net/http :3000) (:4433)
│
│ POST /login ┌──────────────────┐
│ {email, password} │ │ GET /self-service/login/api
│ ───────────────────► │ PostLogin │ ─────────────────────────────►
│ │ handler │ ◄── {flow_id}
│ │ │
│ │ │ POST /self-service/login
│ │ │ ?flow={id}
│ │ │ {method, identifier, password}
│ │ │ ─────────────────────────────►
│ │ │ ◄── {session: {identity: …}}
│ │ │
│ │ • provisions user in PG (first login)
│ │ • creates Redis session
│ 302 → /dashboard └──────────────────┘
│ ◄───────────────────
Components¶
| Component | Port | Role |
|---|---|---|
| Noctuary web | :3000 |
Renders login/register forms, orchestrates Kratos API flows |
| Ory Kratos (public) | :4433 |
Identity API — validates credentials, manages identities |
| Ory Kratos (admin) | :4434 |
Admin API — identity management, never exposed publicly |
| PostgreSQL | :5432 |
Two databases: noctuary (app data) and kratos (identity store) |
| Redis | :6379 |
App sessions (24-hour sliding TTL) |
| MailHog | :8025 |
Dev SMTP sink for verification and recovery emails |
Authentication flows¶
Both flows are server-side only. The browser posts a standard HTML form to Noctuary; Noctuary calls Kratos internally; the browser only ever sees Noctuary URLs.
Login¶
- User visits
GET /login— Noctuary renderslogin.html. - User submits email + password via
POST /login. - Noctuary calls
GET /self-service/login/apion Kratos to initialise a flow and receive aflow_id. - Noctuary calls
POST /self-service/login?flow={id}with{method: "password", identifier, password}. - On success (HTTP 200), Kratos returns the identity object. Noctuary looks up or provisions the user in PostgreSQL, creates a Redis session, and sets a
noctuary_sessioncookie. - On failure (HTTP 400), Kratos returns structured error messages. Noctuary re-renders
login.htmlwith the error inline.
Registration¶
- User visits
GET /register— Noctuary rendersregister.html. - User submits email, password, and organisation name via
POST /register. - Noctuary calls
GET /self-service/registration/apion Kratos to initialise a flow. - Noctuary calls
POST /self-service/registration?flow={id}with{method: "password", traits: {email}, password}. - On success, Kratos creates the identity. Noctuary creates a tenant record (from the org name) and an admin user row in PostgreSQL, then creates a Redis session.
- On failure, the error is displayed inline on the registration form.
Logout¶
POST /auth/logoutdestroys the Redis session and clears thenoctuary_sessioncookie.- The user is redirected to
/login.
Session management¶
Sessions are stored in Redis under the key prefix wsess:.
| Property | Value |
|---|---|
| Key format | wsess:<hex-encoded 32-byte random ID> |
| TTL | 24 hours, sliding (refreshed on each authenticated request) |
| Contents | user_id, tenant_id, org_name, email, role, display_name |
| Cookie name | noctuary_session |
| Cookie flags | HttpOnly, SameSite=Lax |
Revoking a session¶
The session ID is the value of the noctuary_session cookie in the user's browser.
Database schema (auth-relevant tables)¶
tenants¶
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
tier TEXT NOT NULL DEFAULT 'starter',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
A tenant is created on first registration. The name comes from the org_name field on the register form.
users¶
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
identity_id TEXT UNIQUE, -- Kratos identity UUID
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'member',
display_name TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
identity_id is Kratos's stable UUID for the identity. It never changes even if the user updates their email. Noctuary never stores passwords — all credential management happens inside Kratos.
Kratos configuration¶
Kratos is configured via backend/kratos/kratos.yml. Key settings:
serve:
public:
base_url: http://localhost:4433
cors:
enabled: true
allowed_origins: ["http://localhost:3000"]
selfservice:
methods:
password:
enabled: true
flows:
login:
ui_url: http://localhost:3000/login
registration:
ui_url: http://localhost:3000/register
hashers:
algorithm: bcrypt
bcrypt:
cost: 8 # increase to 12+ in production
identity:
default_schema_id: default
schemas:
- id: default
url: file:///etc/config/kratos/identity.schema.json
Identity schema¶
The identity schema (backend/kratos/identity.schema.json) defines email as the only trait, used as the password identifier and for account recovery and verification:
{
"properties": {
"traits": {
"properties": {
"email": {
"type": "string",
"format": "email",
"ory.sh/kratos": {
"credentials": { "password": { "identifier": true } },
"recovery": { "via": "email" },
"verification": { "via": "email" }
}
}
}
}
}
}
Email (SMTP)¶
Kratos sends all transactional email: verification codes and account recovery links.
Development — MailHog¶
In docker-compose.dev.yml, Kratos routes email to MailHog automatically. View captured email at http://localhost:8025. No configuration needed in dev.
Production — real SMTP¶
Update kratos.yml or set via environment variables on the Kratos container:
courier:
smtp:
connection_uri: smtps://user:[email protected]:465/
from_address: [email protected]
from_name: Noctuary
Environment variables reference¶
web/.env (copy from web/.env.example):
| Variable | Required | Description |
|---|---|---|
KRATOS_PUBLIC_URL |
Yes | Base URL of the Kratos public API. Default: http://localhost:4433 |
SESSION_SECRET |
Yes | Min 32 characters. Used to sign session IDs. Rotate to invalidate all sessions. |
DATABASE_URL |
Yes | PostgreSQL connection string for the noctuary database. |
REDIS_URL |
Yes | Redis connection URL. |
Kratos secrets are configured via environment variables on the Kratos container in backend/docker-compose.dev.yml:
| Variable | Description |
|---|---|
SECRETS_COOKIE |
Cookie signing secret for Kratos-internal cookies |
SECRETS_CIPHER |
Encryption key — must be exactly 32 characters (xchacha20-poly1305) |
DSN |
PostgreSQL connection string for the kratos database |
First-run setup¶
cd backend
# 1. Start Postgres, Redis, Kratos, MailHog
# Kratos migrations run automatically via the kratos-migrate service
docker compose -f docker-compose.dev.yml up -d
# 2. Verify Kratos is healthy
curl http://localhost:4433/health/ready
# → {"status":"ok"}
# 3. Start the backend (applies app DB migrations)
op run --env-file=.env -- go run ./cmd/server
# 4. Start the web server
cd ../web
set -a && source .env && set +a
go run ./cmd/server
Visit http://localhost:3000/register to create the first account.
Production considerations¶
TLS¶
In production, both Noctuary and Kratos must be served over HTTPS. Terminate TLS at Caddy, nginx, or a load balancer. Update kratos.yml:
serve:
public:
base_url: https://auth.noctuary.io
admin:
base_url: http://kratos-admin:4434 # internal only, never public
And set KRATOS_PUBLIC_URL=https://auth.noctuary.io in web/.env.
bcrypt cost¶
The dev config uses cost: 8 for fast iteration. In production set it to 12 or higher:
Secrets¶
Both SECRETS_CIPHER and SECRETS_COOKIE must be strong random values in production. SECRETS_CIPHER must be exactly 32 characters:
Store them in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) and inject as environment variables.
Cookie security¶
In production, add Secure: true to the noctuary_session cookie. In web/internal/auth/session.go:
http.SetCookie(w, &http.Cookie{
Name: CookieName,
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: true, // add in production
SameSite: http.SameSiteLaxMode,
})
Kratos admin API¶
The Kratos admin API (:4434) must never be exposed to the public internet. It allows identity creation, deletion, and credential reset without authentication. Bind it to an internal network interface only.
Rate limiting¶
Place a rate-limiting reverse proxy (Caddy, nginx, or a CDN) in front of Noctuary. Kratos has built-in brute-force protection for credential submissions, but edge-level rate limiting reduces load during attacks.
Useful Kratos documentation links¶
| Topic | Link |
|---|---|
| Self-hosting overview | https://www.ory.sh/docs/kratos/self-hosted/overview |
| API flows (server-side) | https://www.ory.sh/docs/kratos/self-service/flows/user-login |
| Identity schema | https://www.ory.sh/docs/kratos/manage-identities/identity-schema |
| Email / SMTP | https://www.ory.sh/docs/kratos/emails-sms/sending-emails-smtp |
| Password hashing | https://www.ory.sh/docs/kratos/concepts/security#password-hashing |
| Account recovery | https://www.ory.sh/docs/kratos/self-service/flows/account-recovery-password-reset |
| Email verification | https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation |