Skip to content

fix(shared-auth): send the Boot error body on a 403 refusal [JDWLABS-499] - #224

Merged
jdwillmsen merged 1 commit into
mainfrom
fix/JDWLABS-499-go-forbidden-body
Sep 6, 2026
Merged

fix(shared-auth): send the Boot error body on a 403 refusal [JDWLABS-499]#224
jdwillmsen merged 1 commit into
mainfrom
fix/JDWLABS-499-go-forbidden-body

Conversation

@jdwillmsen

@jdwillmsen jdwillmsen commented Sep 6, 2026

Copy link
Copy Markdown
Member

Summary

  • The shared Go auth library's 403 refusal (authhttp.WriteForbidden) previously wrote the status and the Access-Denied-Reason header with no body, mirroring what was assumed for both 401 and 403. Measured against a booted usersrole (FilterChainContractParityTests.theAccessDeniedHandlerAnswersWithTheContainerErrorBody), the 403 is not empty: the caller's verified token also authenticates the internal forward to /error, so BasicErrorController runs and answers with Boot's standard error JSON.
  • WriteForbidden now emits that body: Content-Type: application/json and a fixed-order timestamp/status/error/path object, with message absent entirely (server.error.include-message is never). timestamp uses millisecond precision with an explicit numeric offset, matching Boot's Jackson output.
  • WriteUnauthorized (401) is untouched — that forward is refused before BasicErrorController ever runs, so it stays genuinely empty.
  • libs/backend/shared/auth/README.md's error-body section is corrected: the "this library currently reproduces only the 401 half" gap note is removed now that both halves are reproduced.

Test plan

  • TDD: assertContainerErrorBody written first against the old empty-body WriteForbidden, confirmed red, then made green by the implementation.
  • go test ./... -race -count=1 — all packages pass, including authhttp.Example (unchanged, still compiles).
  • go vet ./... — clean.
  • golangci-lint run ./... — no issues found.
  • npx nx run-many -t lint test -p backend-shared-auth — both targets pass.

Exact body emitted (measured with a throwaway probe against WriteForbidden, not committed)

{"timestamp":"2026-09-06T04:31:08.376+00:00","status":403,"error":"Forbidden","path":"/api/profiles/7"}

Content-Type: application/json, Access-Denied-Reason: Not Authorized.

Tests added/changed

  • authhttp.assertContainerErrorBody — new field-by-field fixture comparing the Go 403 body against the shape FilterChainContractParityTests.theAccessDeniedHandlerAnswersWithTheContainerErrorBody measured on the JVM (status/error by value, timestamp/path by shape).
  • TestWriteForbiddenMatchesTheAccessDeniedHandler — updated to assert the new body instead of an empty one.
  • TestAuthorizeWritesTheAccessDeniedShapeOnDenial — updated the same way for the Authorize helper's 403 path.
  • assertRefusalShape — narrowed in its doc comment to the 401 shape, which is unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FvYBM6o7wARm2v9jmDp1yY

CustomAccessDeniedHandler's sendError forwards to /error with the same
verified token that got the caller past authentication, so that dispatch
succeeds and BasicErrorController answers with a real application/json body
instead of the empty one this library previously wrote for both 401 and 403.
WriteForbidden now emits that body: a fixed-order timestamp/status/error/path
object with no message key, since server.error.include-message is never
rather than merely unset. WriteUnauthorized is untouched — the 401 forward is
refused before BasicErrorController runs, so it stays genuinely empty.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FvYBM6o7wARm2v9jmDp1yY
@jdwillmsen
jdwillmsen merged commit 83bc605 into main Sep 6, 2026
22 checks passed
@jdwillmsen
jdwillmsen deleted the fix/JDWLABS-499-go-forbidden-body branch September 6, 2026 04:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant