Files
todo-api/docs/adr/ADR-002-header-auth-for-poc.md
Rene Nochebuena 3fcba82448 feat(todo-api): add full-stack POC demonstrating micro-lib v0.9.0
Runnable REST API exercising every micro-lib tier in a containerless setup: N-layer architecture, SQLite persistence, header-based auth simulating Firebase output, and bit-mask RBAC enforcement.

What's included:
- cmd/todo-api: minimal main delegating to application.Run
- internal/application: full object graph wiring — launcher, sqlite, httpserver, httpmw stack, routes in BeforeStart
- internal/domain: User entity, ResourceTodos constant, PermReadTodo/PermWriteTodo bit positions
- internal/repository: TodoRepository, UserRepository, DBPermissionProvider (SQLite via modernc)
- internal/service: TodoService, UserService with interface-based dependencies
- internal/handler: TodoHandler, UserHandler using httputil adapters and valid for input validation
- internal/middleware: Auth (X-User-ID → rbac.Identity) and Require (bit-mask permission gate)
- logAdapter: bridges logz.Logger.With return type to httpmw.Logger interface
- SQLite schema: users, user_role (bitmask), todos; migrations run in BeforeStart
- Routes: POST /users (open), GET+POST /todos (RBAC), GET /users (RBAC)

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-19 13:55:08 +00:00

2.8 KiB

ADR-002: Header-Based Auth for POC

Status: Accepted Date: 2026-03-18

Context

The todo-api needs authentication so it can demonstrate the RBAC permission system. In production this role would be played by Firebase Auth: the client presents a Firebase ID token, the httpauth-firebase middleware (from httpmw) verifies the JWT, and an rbac.Identity is placed in the request context.

For this POC, introducing Firebase Auth would require:

  • A Firebase project or emulator
  • Token issuance in tests and curl examples
  • JWT verification dependencies in the application

This overhead is not necessary to demonstrate the RBAC flow. The goal is to show the contract between auth middleware and the rest of the stack, not to validate the auth mechanism itself.

Decision

Authentication is performed by reading the X-User-ID request header. The middleware.Auth middleware:

  1. Reads r.Header.Get("X-User-ID").
  2. Returns 401 if the header is absent.
  3. Calls userRepo.FindByID(ctx, uid) to verify the user exists in the database.
  4. Returns 401 if the user is not found (the two failure cases are intentionally indistinguishable to callers — no enumeration of which check failed).
  5. Constructs an rbac.Identity via rbac.NewIdentity(user.ID, user.Name, user.Email) and stores it in the context with rbac.SetInContext.

The output contract — an rbac.Identity in the request context — is identical to what httpauth-firebase would produce. The downstream middleware.Require RBAC guard consumes rbac.Identity from context and has no knowledge of how the identity was established.

The POST /users route is deliberately unguarded. It allows bootstrapping: a new deployment can create its first users (with permission bits set) without prior credentials.

Consequences

  • Replacing header auth with Firebase auth in a production version requires only swapping the Auth middleware. All downstream code (Require, handlers, services, repositories) is unchanged.
  • The header-based approach is intentionally insecure and must not be used in production. This is a POC.
  • The 401 response does not distinguish "missing header" from "user not found" — this is intentional to avoid user enumeration.
  • POST /users is an open endpoint. In a production system this would be protected by Firebase auth or an admin credential. Here it serves as the bootstrap mechanism to populate the users table and assign permission bits.
  • The logAdapter in application bridges logz.Logger.With(...) (which returns logz.Logger) to httpmw.Logger.With(...) (which must return httpmw.Logger). This mismatch arises because httpmw.Logger is a duck-typed interface with a With method that must return httpmw.Logger, not a concrete type. The adapter is defined locally in application — no extra package needed.