Skip to content

fix(auth): 401 (not 403) for anonymous callers so the SPA redirects to login#605

Closed
remyluslosius wants to merge 2 commits into
mainfrom
fix/anonymous-401
Closed

fix(auth): 401 (not 403) for anonymous callers so the SPA redirects to login#605
remyluslosius wants to merge 2 commits into
mainfrom
fix/anonymous-401

Conversation

@remyluslosius

Copy link
Copy Markdown
Contributor

Return 401 (not 403) for anonymous callers on protected endpoints

Why

An expired session surfaced as a dead-end "Failed to load" instead of a login redirect. Root cause: when a session cookie expires in the browser it stops being sent, so the request arrives with no credentials, is bound anonymous, and the RBAC layer returned 403. The SPA redirects to login on a 401, but treats a 403 as "you lack permission" — so it showed a data-load error. (The identity binder already 401s a presented-but-rejected session; this closes the no-credentials gap.)

Change

  • internal/auth/middleware.go (denyPermission): an anonymous caller (IsAnonymous) now gets 401 auth.required with a WWW-Authenticate header; an authenticated caller whose role lacks the permission still gets 403 authz.permission_denied. The authz.permission.denied audit event is emitted in both cases (unchanged).
  • Registered the auth.required error code (api/error_codes.yaml).
  • Frontend needs no changeclient.ts already recognizes a 401 with auth.required (in AUTH_FAILURE_CODES) and fail-closes any 401 to a login redirect.

Contract sweep

The 12 specs/tests that strictly asserted anonymous→403 now assert 401 auth.required (alerts, audit-events-query, fleet-observability, host-system-info, os-intelligence, system-rbac AC-09/AC-15, system/fleet connectivity, discovery/intelligence config). The ~7 specs that already said "401/403" are unchanged. Authenticated-but-unauthorized→403 language preserved throughout.

Verified locally

  • Full internal/auth + internal/server suite: green (exit 0).
  • Specter: 110 specs valid, 100% structural coverage, 0 annotation errors, gofmt clean.

Docs/behavior note: this is the first of the two UX items from the session — the 401 redirect. (The second, treating 401 as re-auth rather than surfacing the data error, is already handled by the existing frontend interceptor.)

An anonymous request (no credentials, or a session cookie that expired in the
browser and is no longer sent) to a protected endpoint now returns 401
auth.required instead of 403. The SPA redirects to login on a 401, so an
expired session surfaces as a clean re-login prompt rather than a dead-end
'failed to load'. An authenticated caller whose role lacks the permission still
gets 403 authz.permission_denied; the audit event is unchanged for both.
The 12 specs/tests that strictly asserted anonymous -> 403 now assert 401
auth.required (alerts, audit-events-query, fleet-observability, host-system-info,
os-intelligence, system-rbac AC-09/AC-15, system/fleet connectivity, discovery/
intelligence config). Authenticated-but-unauthorized -> 403 language preserved.
Specs that already said '401/403' are unchanged.
@remyluslosius

Copy link
Copy Markdown
Contributor Author

Folded into #609 (release: bundle 0.2.0-rc.11) and merged there to avoid the CHANGELOG rebase cascade. Content is on main.

@remyluslosius remyluslosius deleted the fix/anonymous-401 branch June 20, 2026 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant