Skip to content

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

  1. User visits GET /login — Noctuary renders login.html.
  2. User submits email + password via POST /login.
  3. Noctuary calls GET /self-service/login/api on Kratos to initialise a flow and receive a flow_id.
  4. Noctuary calls POST /self-service/login?flow={id} with {method: "password", identifier, password}.
  5. 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_session cookie.
  6. On failure (HTTP 400), Kratos returns structured error messages. Noctuary re-renders login.html with the error inline.

Registration

  1. User visits GET /register — Noctuary renders register.html.
  2. User submits email, password, and organisation name via POST /register.
  3. Noctuary calls GET /self-service/registration/api on Kratos to initialise a flow.
  4. Noctuary calls POST /self-service/registration?flow={id} with {method: "password", traits: {email}, password}.
  5. 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.
  6. On failure, the error is displayed inline on the registration form.

Logout

  1. POST /auth/logout destroys the Redis session and clears the noctuary_session cookie.
  2. 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

redis-cli DEL wsess:<session-id>

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.

# backend/kratos/kratos.yml
courier:
  smtp:
    connection_uri: smtp://mailhog:1025/

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:

hashers:
  bcrypt:
    cost: 12

Secrets

Both SECRETS_CIPHER and SECRETS_COOKIE must be strong random values in production. SECRETS_CIPHER must be exactly 32 characters:

openssl rand -hex 16    # 32 hex chars — valid cipher key
openssl rand -base64 32 # cookie secret

Store them in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) and inject as environment variables.

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.


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