docs(usersrole): describe the real 401/403 shape and CORS preflight in the frozen contracts [JDWLABS-495] - #223
Merged
Conversation
…n the frozen contracts The frozen identity-service and profile-service contracts documented a ContainerError JSON body on every 401 and 403. Measured against a booted usersrole, that only holds for 403: CustomAuthenticationEntryPoint's 401 is genuinely empty (no token means the internal /error forward is refused again before BasicErrorController runs), while CustomAccessDeniedHandler's 403 keeps its verified token across that same forward and gets a real Boot error body. Corrects Unauthorized to empty/no-Content-Type in both specs, refines Forbidden's description to explain why it differs, and adds a CORS preflight section to the contracts README transcribing SecurityConfig's CorsConfiguration and noting preflights bypass authentication entirely. FilterChainContractParityTests now drives real HTTP calls against the booted app to pin both shapes plus the preflight bypass, using a mocked UserRepository/RoleRepository to authenticate a token-holding-but-unprivileged principal without a database schema. The Go shared auth library's README is corrected rather than blindly cleared: its WriteForbidden sends no body for 403, which the verification above shows no longer matches the JVM — flagged as a real, separate gap for its own change, not fixed here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvYBM6o7wARm2v9jmDp1yY
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The frozen
identity-serviceandprofile-servicecontracts documented aContainerErrorJSON body on every 401 and 403. Measured against a bootedusersroleon a real port, that claim only holds for 403:CustomAuthenticationEntryPoint) is genuinely empty —Content-Length: 0, noContent-Type, for everyAcceptvalue. No token means the internal forward to/error(whichsendErrortriggers) is refused again by the same filter chain, soBasicErrorControllernever runs.CustomAccessDeniedHandler) is not empty. The caller here already holds a verified token — only@PreAuthorizerefused it — so that same token authenticates the forwarded/errordispatch, andBasicErrorControllerrenders Boot's standard error body (timestamp,status,error,path— nomessage, sinceserver.error.include-messageisnever). This matches the existingContainerErrorschema exactly.This is a correction to the initial ticket premise (which described both as empty) — verified end-to-end against the real, running application before writing anything down, per this repo's debugging discipline.
Unauthorized(401) as empty-body, no-Content-Type, header-only;Forbidden(403) keepsContainerErrorwith a description explaining why it differs from 401.docs/contracts/README.mdgains a CORS preflight section transcribingSecurityConfig.corsConfigurationSource'sCorsConfiguration(origins, methods, headers, credentials, max-age) and stating preflights bypass authentication entirely (CorsFilterruns ahead of the JWT filter).FilterChainContractParityTestsextended with three new tests driving real HTTP calls against the booted app: the 401 empty-body/no-Content-Type shape, the 403 real container-error body, and a CORS preflight to an authenticated path answering 200 with no token. The 403 test mints a token for a principal with no authorities via a mockedUserRepository/RoleRepository(this suite boots no database schema to authenticate one against for real).libs/backend/shared/auth/README.md's inaccuracy note is corrected rather than blindly removed: the Go library'sWriteForbiddensends no body for 403, which the verification above shows no longer matches the JVM. Flagged as a real, separate gap for its own follow-up change — not fixed here, since changing Go production behavior is out of scope for a contract-documentation PR.served-document-deviations.jsonorcheck-contract-drift.mjs— the drift checker pins error status codes, never body shape, so the pinned error sets still pass unchanged (verified vianpx nx run usersrole:check-contracts).Test plan
npx nx run usersrole:lint— greennpx nx run-many -t lint test check-contracts -p usersrole— green (full test suite, including the 3 new/extended tests)npx nx format:check --all— greentheAuthenticationEntryPointAnswersWithNoBodyAndNoContentType,theAccessDeniedHandlerAnswersWithTheContainerErrorBody,aPreflightToAnAuthenticatedPathAnswers200WithNoToken🤖 Generated with Claude Code
https://claude.ai/code/session_01FvYBM6o7wARm2v9jmDp1yY