- Download swagger-ui-dist@5.32.0 and embed
swagger-ui-bundle.js and swagger-ui.css into
web/static/ so they are served from the same origin
- Update docs.html to reference /static/ paths instead
of unpkg.com CDN URLs
- Add GET /static/swagger-ui-bundle.js and
GET /static/swagger-ui.css handlers serving the
embedded bytes with correct Content-Type headers
- Fixes /docs breakage caused by CSP default-src 'self'
blocking external CDN scripts and stylesheets
Co-authored-by: Junie <junie@jetbrains.com>
- errorlint: use errors.Is for ErrSealed comparisons in vault_test.go
- gofmt: reformat config, config_test, middleware_test with goimports
- govet/fieldalignment: reorder struct fields in vault.go, csrf.go,
detail_test.go, middleware_test.go for optimal alignment
- unused: remove unused newCSRFManager in csrf.go (superseded by
newCSRFManagerFromVault)
- revive/early-return: invert sealed-vault condition in main.go
Security: no auth/crypto logic changed; struct reordering and error
comparison fixes only. newCSRFManager removal is safe — it was never
called; all CSRF construction goes through newCSRFManagerFromVault.
Co-authored-by: Junie <junie@jetbrains.com>
- Replace requireAdmin (role-based) guards on all REST endpoints
with RequirePolicy middleware backed by the existing policy engine;
built-in admin wildcard rule (-1) preserves existing admin behaviour
while operator rules can now grant targeted access to non-admin
accounts (e.g. a system account allowed to list accounts)
- Wire policy engine into Server: loaded from DB at startup,
reloaded after every policy-rule create/update/delete so changes
take effect immediately without a server restart
- Add service_account_delegates table (migration 000008) so a human
account can be delegated permission to issue tokens for a specific
system account without holding the admin role
- Add token-download nonce mechanism: a short-lived (5 min),
single-use random nonce is stored server-side after token issuance;
the browser downloads the token as a file via
GET /token/download/{nonce} (Content-Disposition: attachment)
instead of copying from a flash message
- Add /service-accounts UI page for non-admin delegates
- Add TestPolicyEnforcement and TestPolicyDenyRule integration tests
Security:
- Policy engine uses deny-wins, default-deny semantics; admin wildcard
is a compiled-in built-in and cannot be deleted via the API
- Token download nonces are 128-bit crypto/rand values, single-use,
and expire after 5 minutes; a background goroutine evicts stale entries
- alg header validation and Ed25519 signing unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Include username field in validateResponse struct
- Look up account by UUID and populate username on success
- Add username field to Go client TokenClaims struct
- Fix OpenAPI nullable type syntax (use array form)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- internal/db/accounts.go: add ListAccountsWithTOTP,
ListAllPGCredentials, TOTPRekeyRow, PGRekeyRow, and
Rekey — atomic transaction that replaces master_key_salt,
signing_key_enc/nonce, all TOTP enc/nonce, and all
pg_password enc/nonce in one SQLite BEGIN/COMMIT
- cmd/mciasdb/rekey.go: runRekey — decrypts all secrets
under old master key, prompts for new passphrase (with
confirmation), derives new key from fresh Argon2id salt,
re-encrypts everything, and commits atomically
- cmd/mciasdb/main.go: wire "rekey" command + update usage
- Tests: DB-layer tests for ListAccountsWithTOTP,
ListAllPGCredentials, Rekey (happy path, empty DB, salt
replacement); command-level TestRekeyCommandRoundTrip
verifies full round-trip and adversarially confirms old
key no longer decrypts after rekey
Security: fresh random salt is always generated so a
reused passphrase still produces an independent key; old
and new master keys are zeroed via defer; no passphrase or
key material appears in logs or audit events; the entire
re-encryption is done in-memory before the single atomic
DB write so the database is never in a mixed state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New internal/vault package: thread-safe Vault struct with
seal/unseal state, key material zeroing, and key derivation
- REST: POST /v1/vault/unseal, POST /v1/vault/seal,
GET /v1/vault/status; health returns sealed status
- UI: /unseal page with passphrase form, redirect when sealed
- gRPC: sealedInterceptor rejects RPCs when sealed
- Middleware: RequireUnsealed blocks all routes except exempt
paths; RequireAuth reads pubkey from vault at request time
- Startup: server starts sealed when passphrase unavailable
- All servers share single *vault.Vault by pointer
- CSRF manager derives key lazily from vault
Security: Key material is zeroed on seal. Sealed middleware
runs before auth. Handlers fail closed if vault becomes sealed
mid-request. Unseal endpoint is rate-limited (3/s burst 5).
No CSRF on unseal page (no session to protect; chicken-and-egg
with master key). Passphrase never logged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PEN-01: fix extractBearerFromRequest to validate Bearer prefix
using strings.SplitN + EqualFold; add TestExtractBearerFromRequest
- PEN-02: security headers confirmed present after redeploy (live
probe 2026-03-15)
- PEN-03: accepted — Swagger UI self-hosting disproportionate to risk
- PEN-04: accepted — OpenAPI spec intentionally public
- PEN-05: accepted — gRPC port 9443 intentionally public
- PEN-06: remove RecordLoginFailure from REST TOTP-missing branch
to match gRPC handler (DEF-08); add
TestTOTPMissingDoesNotIncrementLockout
- PEN-07: accepted — per-account hard lockout covers the same threat
- Update AUDIT.md: all 7 PEN findings resolved (4 fixed, 3 accepted)
Security: PEN-01 removed a defence-in-depth gap where any 8+ char
Authorization value was accepted as a Bearer token. PEN-06 closed an
account-lockout-via-omission attack vector on TOTP-enrolled accounts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update package declaration in client.go
- Update error message strings to reference new package name
- Update test package and imports to use new name
- Update README.md documentation and examples with new package name
- All tests pass
Increase token lifetime from 2s to 4s in TestRenewToken to prevent
the token from expiring before the gRPC call completes through bufconn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- REST handleTOTPEnroll now requires password field in request body
- gRPC EnrollTOTP updated with password field in proto message
- Both handlers check lockout status and record failures on bad password
- Updated Go, Python, and Rust client libraries to pass password
- Updated OpenAPI specs with new requestBody schema
- Added TestTOTPEnrollRequiresPassword with no-password, wrong-password,
and correct-password sub-tests
Security: TOTP enrollment now requires the current password to prevent
session-theft escalation to persistent account takeover. Lockout and
failure recording use the same Argon2id constant-time path as login.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add internal/audit package with JSON() and JSONWithRoles() helpers
that use json.Marshal instead of fmt.Sprintf with %q
- Replace all fmt.Sprintf audit detail construction in:
- internal/server/server.go (10 occurrences)
- internal/ui/handlers_auth.go (4 occurrences)
- internal/grpcserver/auth.go (4 occurrences)
- Add tests for the helpers including edge-case Unicode,
null bytes, special characters, and odd argument counts
- Fix broken {"roles":%v} formatting that produced invalid JSON
Security: Audit log detail strings are now constructed via
json.Marshal, which correctly handles all Unicode edge cases
(U+2028, U+2029, null bytes, etc.) that fmt.Sprintf with %q
may mishandle. This prevents potential log injection or parsing
issues in audit event consumers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 50% lifetime elapsed check to REST handleRenew and gRPC RenewToken
- Reject renewal attempts before 50% of token lifetime has elapsed
- Update existing renewal tests to use short-lived tokens with sleep
- Add TestRenewTokenTooEarly tests for both REST and gRPC
Security: Tokens can only be renewed after 50% of their lifetime has
elapsed, preventing indefinite renewal of stolen tokens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add IsAdmin bool to PageData (embedded in all page view structs)
- Remove redundant IsAdmin from DashboardData
- Add isAdmin() helper to derive admin status from request claims
- Set IsAdmin in all page-level handlers that populate PageData
- Wrap admin-only nav links in base.html with {{if .IsAdmin}}
- Add tests: non-admin dashboard/profile hide admin links,
admin dashboard shows them
Security: navigation links to /accounts, /audit, /policies,
and /pgcreds are now only rendered for admin users. Server-side
authorization (requireAdminRole middleware) was already in place;
this change removes the information leak of showing links that
return 403 to non-admin users.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- REST login: change locked account response from HTTP 429
"account_locked" to HTTP 401 "invalid credentials"
- gRPC login: change from ResourceExhausted to Unauthenticated
with "invalid credentials" message
- UI login: change from "account temporarily locked" to
"invalid credentials"
- REST password-change endpoint: same normalization
- Audit logs still record "account_locked" internally
- Added tests in all three layers verifying locked-account
responses are indistinguishable from wrong-password responses
Security: lockout responses now return identical status codes and
messages as wrong-password failures across REST, gRPC, and UI,
preventing user-enumeration via lockout differentiation. Internal
audit logging of lockout events is preserved for operational use.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change default_expiry from 720h (30 days) to 168h (7 days)
in dist/mcias.conf.example and dist/mcias.conf.docker.example
- Update man page, ARCHITECTURE.md, and config.go comment
- Max ceiling validation remains at 30 days (unchanged)
Security: Shorter default token lifetime reduces the window of
exposure if a token is leaked. 7 days balances convenience and
security for a personal SSO. The 30-day max ceiling is preserved
so operators can still override if needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add IssueSystemToken() method in internal/db/accounts.go that wraps
revoke-old, track-new, and upsert-system_tokens in a single SQLite
transaction
- Update handleTokenIssue in internal/server/server.go to use the new
atomic method instead of three separate DB calls
- Update IssueServiceToken in internal/grpcserver/tokenservice.go with
the same fix
- Add TestIssueSystemTokenAtomic test covering first issue and rotation
Security: token issuance now uses a single transaction to prevent
inconsistent state (e.g., old token revoked but new token not tracked)
if a crash occurs between operations. Follows the same pattern as
RenewToken which was already correctly transactional.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add grpcClientIP() helper that mirrors middleware.ClientIP
for proxy-aware IP extraction from gRPC metadata
- Update rateLimitInterceptor to use grpcClientIP with the
TrustedProxy config setting
- Only trust x-forwarded-for/x-real-ip metadata when the
peer address matches the configured trusted proxy
- Add 7 unit tests covering: no proxy, xff, x-real-ip
preference, untrusted peer ignoring headers, no headers
fallback, invalid header fallback, and no peer
Security: gRPC rate limiter now extracts real client IPs
behind a reverse proxy using the same trust model as the
REST middleware (DEF-03). Headers from untrusted peers are
ignored, preventing IP-spoofing for rate-limit bypass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap r.Body with http.MaxBytesReader (1 MiB) in decodeJSON so all
REST API endpoints reject oversized JSON payloads
- Add MaxPasswordLen = 128 constant and enforce it in validate.Password()
to prevent Argon2id DoS via multi-MB passwords
- Add test for oversized JSON body rejection (>1 MiB -> 400)
- Add test for password max length enforcement
Security: decodeJSON now applies the same body size limit the UI layer
already uses, closing the asymmetry. MaxPasswordLen caps Argon2id input
to a reasonable length, preventing CPU-exhaustion attacks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add globalSecurityHeaders middleware wrapping root handler
- Sets X-Content-Type-Options, Strict-Transport-Security, Cache-Control
on all responses (API and UI)
- Add tests verifying headers on /v1/health and /v1/auth/login
Security: API responses previously lacked HSTS, nosniff, and
cache-control headers. The new middleware applies these universally.
Headers are safe for all content types and do not conflict with
the UI's existing securityHeaders middleware.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add noDirListing handler wrapper that returns 404 for directory
requests (paths ending with "/" or empty path) instead of delegating
to http.FileServerFS which would render an index page
- Wrap the static file server in Register() with noDirListing
- Add tests verifying GET /static/ returns 404 and GET /static/style.css
still returns 200
Security: directory listings exposed the names of all static assets,
leaking framework details. The wrapper blocks directory index responses
while preserving normal file serving.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Permissions-Policy header disabling camera, microphone,
geolocation, and payment browser features
- Update assertSecurityHeaders test helper to verify the new header
Security: Permissions-Policy restricts browser APIs that this
application does not use, reducing attack surface from content
injection vulnerabilities. No crypto or auth flow changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change dashboard route from adminGet to authed middleware
- Show account counts and audit events only for admin users
- Show welcome message for non-admin authenticated users
Security: non-admin users cannot access account lists or audit
events; admin-only data is gated by claims.HasRole("admin") in
the handler, not just at the route level.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add requireAdminRole middleware to web UI that checks
claims.HasRole("admin") and returns 403 if absent
- Apply middleware to all admin routes (accounts, policies,
audit, dashboard, credentials)
- Remove redundant inline admin check from handleAdminResetPassword
- Profile routes correctly require only authentication, not admin
Security: The admin/adminGet middleware wrappers only called
requireCookieAuth (JWT validation) but never verified the admin
role. Any authenticated user could access admin endpoints
including role assignment. Fixed by inserting requireAdminRole
into the middleware chain for all admin routes.
- Update knownRoles to include guest, viewer, editor, and commenter
- Replace hardcoded role strings with model constants
- Remove obsolete 'service' role from UI
- All tests pass
- Add RoleGuest, RoleViewer, RoleEditor, and RoleCommenter constants
- Update allowedRoles map to include new roles
- Update ValidateRole error message with complete role list
- All tests pass; build verified
- Add POST /v1/accounts/{id}/roles and DELETE /v1/accounts/{id}/roles/{role} REST endpoints
- Add GrantRole and RevokeRole RPCs to AccountService in gRPC API
- Update OpenAPI specification with new endpoints
- Add grant and revoke subcommands to mciasctl
- Add grant and revoke subcommands to mciasgrpcctl
- Regenerate proto files with new message types and RPCs
- Implement gRPC server methods for granular role management
- All existing tests pass; build verified with goimports
Security: Role changes are audited via EventRoleGranted and EventRoleRevoked events,
consistent with existing SetRoles implementation.
- Add auth/login and auth/logout to mciasgrpcctl, calling
the existing AuthService.Login/Logout RPCs; password is
always prompted interactively (term.ReadPassword), never
accepted as a flag, raw bytes zeroed after use
- Add proto/mcias/v1/policy.proto with PolicyService
(List, Create, Get, Update, Delete policy rules)
- Regenerate gen/mcias/v1/ stubs to include policy
- Implement internal/grpcserver/policyservice.go delegating
to the same db layer as the REST policy handlers
- Register PolicyService in grpcserver.go
- Add policy list/create/get/update/delete to mciasgrpcctl
- Update mciasgrpcctl man page with new commands
Security: auth login uses the same interactive password
prompt pattern as mciasctl; password never appears in
process args, shell history, or logs; raw bytes zeroed
after string conversion (same as REST CLI and REST server).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Trusted proxy config option for proxy-aware IP extraction
used by rate limiting and audit logs; validates proxy IP
before trusting X-Forwarded-For / X-Real-IP headers
- TOTP replay protection via counter-based validation to
reject reused codes within the same time step (±30s)
- RateLimit middleware updated to extract client IP from
proxy headers without IP spoofing risk
- New tests for ClientIP proxy logic (spoofed headers,
fallback) and extended rate-limit proxy coverage
- HTMX error banner script integrated into web UI base
- .gitignore updated for mciasdb build artifact
Security: resolves CRIT-01 (TOTP replay attack) and
DEF-03 (proxy-unaware rate limiting); gRPC TOTP
enrollment aligned with REST via StorePendingTOTP
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Web UI admin password reset now enforces admin role
server-side (was cookie-auth + CSRF only; any logged-in
user could previously reset any account's password)
- Added self-service password change UI at GET/PUT /profile:
current_password + new_password + confirm_password;
server-side equality check; lockout + Argon2id verification;
revokes all other sessions on success
- password_change_form.html fragment and profile.html page
- Nav bar actor name now links to /profile
- policy: ActionChangePassword + default rule -7 allowing
human accounts to change their own password
- openapi.yaml: built-in rules count updated to -7
Migration recovery:
- mciasdb schema force --version N: new subcommand to clear
dirty migration state without running SQL (break-glass)
- schema subcommands bypass auto-migration on open so the
tool stays usable when the database is dirty
- Migrate(): shim no longer overrides schema_migrations
when it already has an entry; duplicate-column error on
the latest migration is force-cleaned and treated as
success (handles columns added outside the runner)
Security:
- Admin role is now validated in handleAdminResetPassword
before any DB access; non-admin receives 403
- handleSelfChangePassword follows identical lockout +
constant-time Argon2id path as the REST self-service
handler; current password required to prevent
token-theft account takeover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- internal/db/migrations/: five embedded SQL files containing
the migration SQL previously held as Go string literals.
Files follow the NNN_description.up.sql naming convention
required by golang-migrate's iofs source.
- internal/db/migrate.go: rewritten to use
github.com/golang-migrate/migrate/v4 with the
database/sqlite driver (modernc.org/sqlite, pure Go) and
source/iofs for compile-time embedded SQL.
- newMigrate() opens a dedicated *sql.DB so m.Close() does
not affect the caller's shared connection.
- Migrate() includes a compatibility shim: reads the legacy
schema_version table and calls m.Force(v) before m.Up()
so existing databases are not re-migrated.
- LatestSchemaVersion promoted from var to const.
- internal/db/db.go: added path field to DB struct; Open()
translates ':memory:' to a named shared-cache URI
(file:mcias_N?mode=memory&cache=shared) so the migration
runner can open a second connection to the same in-memory
database without sharing the handle that golang-migrate
will close on teardown.
- go.mod: added golang-migrate/migrate/v4 v4.19.1 (direct).
All callers unchanged. All tests pass; golangci-lint clean.
* web/templates/pgcreds.html: New Credentials card is now always
rendered; Add Credentials toggle button reveals the create form
(hidden by default). Shows a message when all system accounts
already have credentials. Previously the card was hidden when
UncredentialedAccounts was empty.
* internal/ui/ui.go: added ActorName string field to PageData;
added actorName(r) helper resolving username from JWT claims
via DB lookup, returns empty string if unauthenticated.
* internal/ui/handlers_*.go: all full-page PageData constructors
now pass ActorName: u.actorName(r).
* web/templates/base.html: nav bar renders actor username as a
muted label before the Logout button when logged in.
* web/static/style.css: added .nav-actor rule (muted grey, 0.85rem).
- internal/ui/ui.go: add PGCred, Tags to AccountDetailData; register
PUT /accounts/{id}/pgcreds and PUT /accounts/{id}/tags routes; add
pgcreds_form.html and tags_editor.html to shared template set; remove
unused AccountTagsData; fix fieldalignment on PolicyRuleView, PoliciesData
- internal/ui/handlers_accounts.go: add handleSetPGCreds — encrypts
password via crypto.SealAESGCM, writes audit EventPGCredUpdated, renders
pgcreds_form fragment; password never echoed; load PG creds and tags in
handleAccountDetail
- internal/ui/handlers_policy.go: fix handleSetAccountTags to render with
AccountDetailData instead of removed AccountTagsData
- internal/ui/ui_test.go: add 5 PG credential UI tests
- web/templates/fragments/pgcreds_form.html: new fragment — metadata display
+ set/replace form; system accounts only; password write-only
- web/templates/fragments/tags_editor.html: new fragment — textarea editor
with HTMX PUT for atomic tag replacement
- web/templates/fragments/policy_form.html: rewrite to use structured fields
matching handleCreatePolicyRule (roles/account_types/actions multi-select,
resource_type, subject_uuid, service_names, required_tags, checkbox)
- web/templates/policies.html: new policies management page
- web/templates/fragments/policy_row.html: new HTMX table row with toggle
and delete
- web/templates/account_detail.html: add Tags card and PG Credentials card
- web/templates/base.html: add Policies nav link
- internal/server/server.go: remove ~220 lines of duplicate tag/policy
handler code (real implementations are in handlers_policy.go)
- internal/policy/engine_wrapper.go: fix corrupted source; use errors.New
- internal/db/policy_test.go: use model.AccountTypeHuman constant
- cmd/mciasctl/main.go: add nolint:gosec to int(os.Stdin.Fd()) calls
- gofmt/goimports: db/policy_test.go, policy/defaults.go,
policy/engine_test.go, ui/ui.go, cmd/mciasctl/main.go
- fieldalignment: model.PolicyRuleRecord, policy.Engine, policy.Rule,
policy.RuleBody, ui.PolicyRuleView
Security: PG password encrypted AES-256-GCM with fresh random nonce before
storage; plaintext never logged or returned in any response; audit event
written on every credential write.
- web/static/htmx.min.js: replace placeholder stub with
htmx 2.0.4 (downloaded from unpkg.com). The placeholder
only logged a console warning; no HTMX features worked,
so form submissions fell back to native POSTs and the
account_row fragment was returned as a raw HTML body
rather than spliced into the table. This was the root
cause of account creation appearing to 'do nothing'.
- internal/ui/ui.go: add pgcreds_form.html to shared
template list; add PUT /accounts/{id}/pgcreds route;
reorder AccountDetailData fields so embedded PageData
does not shadow Account.
- internal/ui/handlers_accounts.go: add handleSetPGCreds
handler — encrypts the submitted password with AES-256-GCM
using the server master key before storage, validates
system-account-only constraint, re-reads and re-renders
the fragment after save. Add PGCred field population to
handleAccountDetail.
- internal/ui/ui_test.go: add tests for account creation,
role management, and PG credential handlers.
- web/templates/account_detail.html: add Postgres
Credentials card for system accounts.
- web/templates/fragments/pgcreds_form.html: new fragment
for the PG credentials form; CSRF token is supplied via
the body-level hx-headers attribute in base.html.
Security: PG password is encrypted with AES-256-GCM
(crypto.SealAESGCM) before storage; a fresh nonce is
generated per call; the plaintext is never logged or
returned in responses.
- Corrected lockout logic (`IsLockedOut`) to properly evaluate failed login thresholds within the rolling window, ensuring stale attempts outside the window do not trigger lockout.
- Updated test passwords in `grpcserver_test.go` to comply with 12-character minimum requirement.
- Reformatted import blocks with `goimports` to address lint warnings.
- Verified all tests pass and linter is clean.
- Added failed login tracking for account lockout enforcement in `db` and `ui` layers; introduced `failed_logins` table to store attempts, window start, and attempt count.
- Updated login checks in `grpcserver/auth.go` and `ui/handlers_auth.go` to reject requests if the account is locked.
- Added immediate failure counter reset on successful login.
- Implemented username length and character set validation (F-12) and minimum password length enforcement (F-13) in shared `validate` package.
- Updated account creation and edit flows in `ui` and `grpcserver` layers to apply validation before hashing/processing.
- Added comprehensive unit tests for lockout, validation, and related edge cases.
- Updated `AUDIT.md` to mark F-08, F-12, and F-13 as fixed.
- Updated `openapi.yaml` to reflect new validation and lockout behaviors.
Security: Prevents brute-force attacks via lockout mechanism and strengthens defenses against weak and invalid input.
- auth/auth.go: add DummyHash() which uses sync.Once to compute
HashPassword("dummy-password-for-timing-only", DefaultArgonParams())
on first call; subsequent calls return the cached PHC string;
add sync to imports
- auth/auth_test.go: TestDummyHashIsValidPHC verifies the hash
parses and verifies correctly; TestDummyHashIsCached verifies
sync.Once behaviour; TestDummyHashMatchesDefaultParams verifies
embedded m/t/p match DefaultArgonParams()
- server/server.go, grpcserver/auth.go, ui/ui.go: replace five
hardcoded PHC strings with auth.DummyHash() calls
- AUDIT.md: mark F-07 as fixed
Security: the previous hardcoded hash used a 6-byte salt and
6-byte output ("testsalt"/"testhash" in base64), which Argon2id
verifies faster than a real 16-byte-salt / 32-byte-output hash.
This timing gap was measurable and could aid user enumeration.
auth.DummyHash() uses identical parameters and full-length salt
and output, so dummy verification timing matches real timing
exactly, regardless of future parameter changes.
- ui/handlers_accounts.go (handleIssueSystemToken): call
GetSystemToken before issuing; if one exists, call
RevokeToken(existing.JTI, "rotated") before TrackToken
and SetSystemToken for the new token; mirrors the pattern
in REST handleTokenIssue and gRPC IssueServiceToken
- db/db_test.go: TestSystemTokenRotationRevokesOld verifies
the full rotation flow: old JTI revoked with reason
"rotated", new JTI tracked and active, GetSystemToken
returns the new JTI
- AUDIT.md: mark F-16 as fixed
Security: without this fix an old system token remained valid
after rotation until its natural expiry, giving a leaked or
stolen old token extra lifetime. With the revocation the old
JTI is immediately marked in token_revocation so any validator
checking revocation status rejects it.