Files
silo/docs/AUTH.md
Forbes 4f0107f1b2 feat(auth): add authentication, RBAC, API tokens, and default admin
Add a complete authentication and authorization system to Silo with
three pluggable backends (local bcrypt, LDAP/FreeIPA, OIDC/Keycloak),
session management, API token support, and role-based access control.

Authentication backends:
- Local: bcrypt (cost 12) password verification against users table
- LDAP: FreeIPA simple bind with group-to-role mapping
- OIDC: Keycloak redirect flow with realm role mapping
- Backends are tried in order; users upserted to DB on first login

Session and token management:
- PostgreSQL-backed sessions via alexedwards/scs + pgxstore
- Opaque API tokens (silo_ prefix, SHA-256 hashed, shown once)
- 24h session lifetime, HttpOnly/SameSite=Lax/Secure cookies

Role-based access control (admin > editor > viewer):
- RequireAuth middleware: Bearer token -> session -> redirect/401
- RequireRole middleware: per-route-group minimum role enforcement
- CSRF protection via justinas/nosurf on web forms, API exempt
- CORS locked to configured origins when auth enabled

Route restructuring:
- Public: /health, /ready, /login, /auth/oidc, /auth/callback
- Web (auth + CSRF): /, /projects, /schemas, /settings
- API read (viewer): GET /api/**
- API write (editor): POST/PUT/PATCH/DELETE /api/**

User context wiring:
- created_by/updated_by columns on items, projects, relationships
- All create/update handlers populate tracking fields from context
- CSV and BOM import handlers pass authenticated username
- Revision creation tracks user across all code paths

Default admin account:
- Configurable via auth.local.default_admin_username/password
- Env var overrides: SILO_ADMIN_USERNAME, SILO_ADMIN_PASSWORD
- Idempotent: created on first startup, skipped if exists

CLI and FreeCAD plugin:
- silo token create/list/revoke subcommands (HTTP API client)
- FreeCAD SiloClient sends Bearer token on all requests
- Token read from ApiToken preference or SILO_API_TOKEN env var

Web UI:
- Login page (Catppuccin Mocha themed, OIDC button conditional)
- Settings page with account info and API token management
- User display name, role badge, and logout button in header
- One-time token display banner with copy-to-clipboard

Database (migration 009):
- users table with role, auth_source, oidc_subject, password_hash
- api_tokens table with SHA-256 hash, prefix, expiry, scopes
- sessions table (scs pgxstore schema)
- audit_log table (schema ready for future use)
- created_by/updated_by ALTER on items, relationships, projects

New dependencies: scs/v2, scs/pgxstore, go-oidc/v3, go-ldap/v3,
justinas/nosurf, golang.org/x/oauth2
2026-01-31 11:20:12 -06:00

8.4 KiB

Silo Authentication Architecture

Overview

Silo supports three authentication backends that can be enabled independently or in combination:

Backend Use Case Config Key
Local Username/password stored in Silo's database (bcrypt) auth.local
LDAP FreeIPA / Active Directory via LDAP bind auth.ldap
OIDC Keycloak or any OpenID Connect provider (redirect flow) auth.oidc

When authentication is disabled (auth.enabled: false), all routes are open and a synthetic developer user with the admin role is injected into every request.

Authentication Flow

Browser (Session-Based)

User -> /login (GET)      -> Renders login form
User -> /login (POST)     -> Validates credentials via backends
                           -> Creates server-side session (PostgreSQL)
                           -> Sets silo_session cookie
                           -> Redirects to / or ?next= URL

User -> /auth/oidc (GET)  -> Generates state, stores in session
                           -> Redirects to Keycloak authorize endpoint
Keycloak -> /auth/callback -> Verifies state, exchanges code for token
                           -> Extracts claims, upserts user in DB
                           -> Creates session, redirects to /

API Client (Token-Based)

Client -> Authorization: Bearer silo_<hex> -> SHA-256 hash lookup
                                           -> Validates expiry + user active
                                           -> Injects user into context

Both paths converge at the RequireAuth middleware, which injects an auth.User into the request context. All downstream handlers use auth.UserFromContext(ctx) to access the authenticated user.

Database Schema

users

The single identity table that all backends resolve to. LDAP and OIDC users are upserted on first login.

Column Type Notes
id UUID Primary key
username TEXT Unique. FreeIPA uid or OIDC preferred_username
display_name TEXT Human-readable name
email TEXT Not unique-constrained
password_hash TEXT NULL for LDAP/OIDC-only users (bcrypt cost 12)
auth_source TEXT local, ldap, or oidc
oidc_subject TEXT Stable OIDC sub claim (unique partial index)
role TEXT admin, editor, or viewer
is_active BOOLEAN Deactivated users are rejected at middleware
last_login_at TIMESTAMPTZ Updated on each successful login
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

api_tokens

Column Type Notes
id UUID Primary key
user_id UUID FK to users, CASCADE on delete
name TEXT Human-readable label
token_hash TEXT SHA-256 of raw token (unique)
token_prefix TEXT silo_ + 8 hex chars for display
scopes TEXT[] Reserved for future fine-grained permissions
last_used_at TIMESTAMPTZ Updated asynchronously on use
expires_at TIMESTAMPTZ NULL = never expires
created_at TIMESTAMPTZ

Raw token format: silo_ + 64 hex characters (32 random bytes). Shown once at creation. Only the SHA-256 hash is stored.

sessions

Required by alexedwards/scs pgxstore:

Column Type Notes
token TEXT Primary key (session ID)
data BYTEA Serialized session data
expiry TIMESTAMPTZ Indexed for cleanup

Session data contains user_id and username. Cookie: silo_session, HttpOnly, SameSite=Lax, 24h lifetime. Secure flag is set when auth.enabled is true.

audit_log

Column Type Notes
id UUID Primary key
timestamp TIMESTAMPTZ Indexed DESC
user_id UUID FK to users, SET NULL on delete
username TEXT Preserved after user deletion
action TEXT create, update, delete, login, etc.
resource_type TEXT item, revision, project, relationship
resource_id TEXT
details JSONB Arbitrary structured data
ip_address TEXT

User Tracking Columns

Migration 009 adds created_by and updated_by TEXT columns to:

  • items (created_by, updated_by)
  • relationships (created_by, updated_by)
  • projects (created_by)
  • sync_log (triggered_by)

These store the username string (not a foreign key) so audit records survive user deletion and dev mode uses "dev".

Role Model

Three roles with a strict hierarchy:

admin > editor > viewer
Permission viewer editor admin
Read items, projects, schemas, BOMs Yes Yes Yes
Create/update items and revisions No Yes Yes
Upload files No Yes Yes
Manage BOMs No Yes Yes
Create/update projects No Yes Yes
Import CSV / BOM CSV No Yes Yes
Generate part numbers No Yes Yes
Manage own API tokens Yes Yes Yes
User management (future) No No Yes

Role enforcement uses auth.RoleSatisfies(userRole, minimumRole) which checks the hierarchy. A user with admin satisfies any minimum role.

Role Mapping from External Sources

LDAP/FreeIPA: Mapped from group membership. Checked in priority order (admin > editor > viewer). First match wins.

ldap:
  role_mapping:
    admin:
      - "cn=silo-admins,cn=groups,cn=accounts,dc=kindred,dc=internal"
    editor:
      - "cn=silo-users,cn=groups,cn=accounts,dc=kindred,dc=internal"
      - "cn=engineers,cn=groups,cn=accounts,dc=kindred,dc=internal"
    viewer:
      - "cn=silo-viewers,cn=groups,cn=accounts,dc=kindred,dc=internal"

OIDC/Keycloak: Mapped from realm_access.roles in the ID token.

oidc:
  admin_role: "silo-admin"
  editor_role: "silo-editor"
  default_role: "viewer"

Roles are re-evaluated on every external login. If an LDAP user's group membership changes in FreeIPA, their Silo role updates on their next login.

User Lifecycle

Event What Happens
First LDAP login User row created with auth_source=ldap, role from group mapping
First OIDC login User row created with auth_source=oidc, oidc_subject set
Subsequent external login display_name, email, role updated; last_login_at updated
Local account creation Admin creates user with username, password, role
Deactivation is_active=false — sessions and tokens rejected at middleware
Password change Only for auth_source=local users

Default Admin Account

On startup, if auth.local.default_admin_username and auth.local.default_admin_password are both set, the daemon checks for an existing user with that username. If none exists, it creates a local admin account. This is idempotent — subsequent startups skip creation.

auth:
  local:
    enabled: true
    default_admin_username: "admin"
    default_admin_password: ""  # Use SILO_ADMIN_PASSWORD env var

Set via environment variables for production:

export SILO_ADMIN_USERNAME=admin
export SILO_ADMIN_PASSWORD=<strong-password>

API Token Security

  • Raw token: silo_ + 64 hex characters (32 bytes of crypto/rand)
  • Storage: SHA-256 hash only — the raw token cannot be recovered from the database
  • Display prefix: silo_ + first 8 hex characters (for identification in UI)
  • Tokens inherit the owning user's role. If the user is deactivated, all their tokens stop working
  • Revocation is immediate (row deletion)
  • last_used_at is updated asynchronously to avoid slowing down API requests

Dev Mode

When auth.enabled: false:

  • No login is required
  • A synthetic user is injected into every request:
    • Username: dev
    • Role: admin
    • Auth source: local
  • created_by fields are set to "dev"
  • CORS allows all origins
  • Session cookies are not marked Secure

Security Considerations

  • Password hashing: bcrypt with cost factor 12
  • Token entropy: 256 bits (32 bytes from crypto/rand)
  • LDAP: Always use ldaps:// (TLS). tls_skip_verify is available but should only be used for testing
  • OIDC state parameter: 128 bits, stored server-side in session, verified on callback
  • Session cookies: HttpOnly, SameSite=Lax, Secure when auth enabled
  • CSRF protection: nosurf library on all web form routes. API routes exempt (use Bearer tokens instead)
  • CORS: Locked down to configured origins when auth is enabled. Credentials allowed for session cookies