Files
create/docs/src/silo-server/AUTH_MIDDLEWARE.md
forbes 87a0af0b0f phase 1: copy Kindred-only files onto upstream/main (FreeCAD 1.2.0-dev)
Wholesale copy of all Kindred Create additions that don't conflict with
upstream FreeCAD code:

- kindred-icons/ (1444 Catppuccin Mocha SVG icon overrides)
- src/Mod/Create/ (Kindred Create workbench)
- src/Gui/ Kindred source files (FileOrigin, OriginManager,
  OriginSelectorWidget, CommandOrigin, BreadcrumbToolBar, EditingContext)
- src/Gui/Icons/ (Kindred branding and silo icons)
- src/Gui/PreferencePacks/KindredCreate/
- src/Gui/Stylesheets/ (KindredCreate.qss, images_dark-light/)
- package/ (rattler-build recipe)
- docs/ (architecture, guides, specifications)
- .gitea/ (CI workflows, issue templates)
- mods/silo, mods/ztools submodules
- .gitmodules (Kindred submodule URLs)
- resources/ (kindred-create.desktop, kindred-create.xml)
- banner-logo-light.png, CONTRIBUTING.md
2026-02-13 14:03:58 -06:00

6.0 KiB

Silo Auth Middleware

Middleware Chain

Every request passes through this middleware stack in order:

RequestID          Assigns X-Request-ID header
    |
RealIP             Extracts client IP from X-Forwarded-For
    |
RequestLogger      Logs method, path, status, duration (zerolog)
    |
Recoverer          Catches panics, logs stack trace, returns 500
    |
CORS               Validates origin, sets Access-Control headers
    |
SessionLoadAndSave Loads session from PostgreSQL, saves on response
    |
[route group middleware applied per-group below]

Route Groups

Public (no auth)

GET  /health
GET  /ready
GET  /login
POST /login
POST /logout
GET  /auth/oidc
GET  /auth/callback

No authentication or CSRF middleware. The login form includes a CSRF hidden field but nosurf is not enforced on these routes (they are outside the CSRF-protected group).

Web UI (auth + CSRF)

RequireAuth -> CSRFProtect -> Handler

GET  /                       Items page
GET  /projects               Projects page
GET  /schemas                Schemas page
GET  /settings               Settings page (account info, tokens)
POST /settings/tokens        Create API token (form)
POST /settings/tokens/{id}/revoke   Revoke token (form)

Both RequireAuth and CSRFProtect are applied. Form submissions must include a csrf_token hidden field with the value from nosurf.Token(r).

API (auth, no CSRF)

RequireAuth -> [RequireRole where needed] -> Handler

GET    /api/auth/me                  Current user info (viewer)
GET    /api/auth/tokens              List tokens (viewer)
POST   /api/auth/tokens              Create token (viewer)
DELETE /api/auth/tokens/{id}         Revoke token (viewer)

GET    /api/schemas/*                Read schemas (viewer)
POST   /api/schemas/*/segments/*     Modify schema values (editor)

GET    /api/projects/*               Read projects (viewer)
POST   /api/projects                 Create project (editor)
PUT    /api/projects/{code}          Update project (editor)
DELETE /api/projects/{code}          Delete project (editor)

GET    /api/items/*                  Read items (viewer)
POST   /api/items                    Create item (editor)
PUT    /api/items/{partNumber}       Update item (editor)
DELETE /api/items/{partNumber}       Delete item (editor)
POST   /api/items/{partNumber}/file  Upload file (editor)
POST   /api/items/import             CSV import (editor)

GET    /api/items/{partNumber}/bom/* Read BOM (viewer)
POST   /api/items/{partNumber}/bom   Add BOM entry (editor)
PUT    /api/items/{partNumber}/bom/* Update BOM entry (editor)
DELETE /api/items/{partNumber}/bom/* Delete BOM entry (editor)

POST   /api/generate-part-number     Generate PN (editor)

GET    /api/integrations/odoo/*      Read Odoo config (viewer)
PUT    /api/integrations/odoo/*      Modify Odoo config (editor)
POST   /api/integrations/odoo/*      Odoo sync operations (editor)

API routes are exempt from CSRF (they use Bearer token auth). CORS credentials are allowed so browser-based API clients with session cookies work.

RequireAuth

internal/api/middleware.go

Authentication check order:

  1. Auth disabled? Inject synthetic dev user (admin role) and continue
  2. Bearer token? Extract from Authorization: Bearer silo_... header, validate via auth.Service.ValidateToken(). On success, inject user into context
  3. Session cookie? Read user_id from scs session, look up user via auth.Service.GetUserByID(). On success, inject user into context. On stale session (user not found), destroy session
  4. None of the above?
    • API requests (/api/*): Return 401 Unauthorized JSON
    • Web requests: Redirect to /login?next=<current-path>

RequireRole

internal/api/middleware.go

Applied as per-group middleware on routes that require a minimum role:

r.Use(server.RequireRole(auth.RoleEditor))

Checks auth.RoleSatisfies(user.Role, minimum) against the hierarchy admin > editor > viewer. Returns:

  • 401 Unauthorized if no user in context (should not happen after RequireAuth)
  • 403 Forbidden with message "Insufficient permissions: requires <role> role" if role is too low

CSRFProtect

internal/api/middleware.go

Wraps the justinas/nosurf library:

  • Cookie: csrf_token, HttpOnly, SameSite=Lax, Secure when auth enabled
  • Exempt paths: /api/*, /health, /ready
  • Form field name: csrf_token
  • Failure: Returns 403 Forbidden with "CSRF token validation failed"

Templates inject the token via {{.CSRFToken}} which is populated from nosurf.Token(r).

CORS Configuration

Configured in internal/api/routes.go:

Setting Auth Disabled Auth Enabled
Allowed Origins * From auth.cors.allowed_origins config
Allow Credentials false true (needed for session cookies)
Allowed Methods GET, POST, PUT, PATCH, DELETE, OPTIONS Same
Allowed Headers Accept, Authorization, Content-Type, X-CSRF-Token, X-Request-ID Same
Max Age 300 seconds 300 seconds

FreeCAD uses direct HTTP (not browser), so CORS does not affect it. Browser-based tools on other origins need their origin in the allowed list.

Request Flow Examples

Browser Login

GET /projects
  -> RequestLogger
  -> CORS (pass)
  -> Session (loads empty session)
  -> RequireAuth: no token, no session user_id
  -> Redirect 303 /login?next=/projects

POST /login (username=alice, password=...)
  -> RequestLogger
  -> CORS (pass)
  -> Session (loads)
  -> HandleLogin: auth.Authenticate(ctx, "alice", password)
  -> Session: put user_id, renew token
  -> Redirect 303 /projects

GET /projects
  -> Session (loads, has user_id)
  -> RequireAuth: session -> GetUserByID -> inject alice
  -> CSRFProtect: GET request, passes
  -> HandleProjectsPage: renders with alice's info

API Token Request

GET /api/items
  Authorization: Bearer silo_a1b2c3d4...

  -> RequireAuth: Bearer token found
  -> ValidateToken: SHA-256 hash lookup, check expiry, check user active
  -> Inject user into context
  -> HandleListItems: returns JSON