ReviewOS

zig-utils/zig-waf

[WAF-57] Implement local authentication, RBAC, API tokens, sessions, and OIDC

#58
Closed chris opened this 9 days ago ·
9 days ago

Objective

Implement bootstrap, Argon2id passwords, secure sessions, CSRF, recovery, permissions, token scopes, OIDC discovery and PKCE, state and nonce validation, and administrative audit events.

Deliverables

  • Implement the complete scoped behavior described above.
  • Keep request-path work independent from UI, PostgreSQL, and blocking audit I/O.
  • Use Zig 0.17-dev and dependencies pinned through Pantry; do not add Git submodules.
  • Keep public APIs explicit about ownership, resource limits, errors, security policy, and compatibility.
  • Add focused unit, integration, differential, fuzz, benchmark, and documentation coverage as applicable.

Acceptance criteria

  • The scoped implementation and tests are merged on main in small conventional commits.
  • Zig formatting, focused tests, and the relevant CI suite pass.
  • Compatibility claims have executable evidence and no unexplained skips or silent fallbacks.
  • Untrusted inputs have deterministic bounds; hot paths have benchmark evidence.
  • Documentation and the machine-readable compatibility matrix are updated.

Relationships

Blocked by

Blocks

Baselines

  • ModSecurity 3.0.16
  • Coraza 3.7.0
  • OWASP CRS 4.28.0
  • libinjection 4.0.0
  • Zig 0.17-dev resolved and locked through Pantry
9 days ago

Complete. Identity lives in src/fleet_auth.zig, ID-token verification in src/oidc.zig.

Local authentication — argon2id at RFC 9106's second recommended cost (64 MiB, three passes), affordable for an interactive login and expensive enough that a stolen hash is not a password. The PHC encoding carries its own parameters, so a hash still verifies after the cost is raised. Failure is uniform: unknown email, disabled account, federated account with no password, and wrong password are all null, because which one it was is what an attacker probing for accounts wants to learn.

RBAC — a total function from role to enumerated action. A new action has to be classified to be permitted anywhere, and a role that does not parse grants nothing rather than defaulting to the least or most privileged.

API tokens — 256 bits from the OS secure source, URL-safe so a token can be a bearer header unencoded, stored only as a SHA-256 hash. Hashing choice is deliberate: argon2 protects a low-entropy human password, but putting it on every API request would be a denial-of-service vector for no security, since a 256-bit random token is not guessable. Expiry and revocation are checked by authentication itself; revocation is recorded rather than deleted so a token that was used stays accounted for; a successful check stamps last_used_at so an unused token is visible when deciding what to revoke; and a token cannot be issued for an unknown user.

Sessions — always bounded (there is no unlimited session lifetime), individually revocable for a logout, and revocable in bulk for a password change. Expiry is enforced by authentication rather than by housekeeping having run, so pruneExpired is maintenance and not a security control.

Disabling a user invalidates every token and session it holds without having to find them — the authorization queries join through the user, so there is no revocation list to keep in step.

OIDC — ID-token verification answering one question: does this token prove that this provider authenticated this subject for this console, right now. The algorithm is a property of the key, never of the token's header, so the two attacks that matter cannot work — alg: none, and asking to be verified as HMAC so the provider's public key becomes the shared secret. Both are tested, along with a forged signature, a tampered payload, a wrong issuer, another client's audience, expiry with leeway, a token issued in the future, a replayed or absent nonce (compared in constant time), and every claim missing or of the wrong type. RS256 and ES256, at the key sizes providers publish.

Accounts link by sub, not email: an email address can be reassigned, and at that moment a linkage by email hands the new holder the old account.

Scope boundary — discovery and JWKS fetching are deliberately not here. They are HTTP with a destination policy, which the caller already owns (the same separation as remote_rules.zig), and keeping them out makes verification a pure function of its inputs, testable exhaustively without a network. The HTTP surface belongs with the API contract in #49.

Commits: aa5b345 (users, RBAC, tokens, sessions), df169a2 (OIDC), 7b6e8e1 (schema), 328ceef (docs).

Sign in to comment on this issue.