feat(i18n): migrate OIDC bind flow to ResponseErrorL (Part of #170)#223
Conversation
β¦ndpoints Endpoints with no legacy fixed-400 clients can keep their real HTTP status while still emitting the localized dual envelope. Shares respondL with ResponseErrorL (which stays pinned to 400 during the D14 window); zero renderer change since the renderer already reads spec.TransportStatus.
12 bind-flow codes plus reuse of shared rate.limited/internal/auth.required. The single 401 invalid_credentials code is generic by design (anti-enumeration); the 503 service-unavailable code is Internal=true so it never leaks a message. Adds the en-US marker and zh-CN runtime translations.
All 43 raw c.AbortWithStatusJSON(errMsg) sites in api_bind.go now route through respondBindError, preserving the real wire status (400/401/409/410/422/429/503), the writeAudit/metric side effects, and the anti-enumeration contract (ErrBindAuthRejected -> single generic 401). baseline.txt ratchets api_bind.go to 0; api.go is marked EXEMPT since its OAuth2/OIDC protocol endpoints keep raw responses by design.
The 5 errMsg(err.Error()) sites in authorize (5xx random/PKCE/auth-URL failures and return_to validation) no longer reflect internal or user-supplied detail to the browser; specifics now go to zap.Error only. Protocol endpoints (authorize/callback/logout) keep raw responses by design and are not migrated to the i18n envelope.
Source guard forbids legacy responses in api_bind.go (api.go exempt). Contract tests assert the real wire status, error.code, dual envelope, anti-enumeration (generic 401), and zh-CN translation via the injected renderer.
Jerry-Xin
left a comment
There was a problem hiding this comment.
Scope check passed; this PR is relevant to octo-server and the OIDC/i18n backend rollout. I found no blocking correctness or security issues.
π¬ Non-blocking
- π΅ Suggestion β
modules/oidc/api_i18n.go:26:codeSharedAuthRequiredis initialized but unused. Removing it would avoid an unnecessary init-time dependency and keep this helper file tighter. - π‘ Warning β
pkg/errcode/oidc.go:34-39withpkg/i18n/renderer.go:68-71:ErrOIDCBindServiceUnavailablehas a specific localized message and translation entries, butInternal: truemeans the renderer will always emit the shared internal-error message instead. That appears intentional for 5xx hygiene, but the code-specific translation is currently unreachable.
β Highlights
pkg/httperr/respond.go:22-73cleanly preserves the legacyResponseErrorLbehavior while adding a scoped semantic-status facade.modules/oidc/api_bind.go:61-617consistently routes bind errors throughrespondBindErrorand preserves the existing status-code semantics.modules/oidc/api_bind_i18n_test.go:25-178adds useful guards for raw response regressions, envelope shape, semantic status, anti-enumeration, and zh-CN localization.modules/oidc/api.go:317-368closes the obviouserr.Error()reflection/internal-error leaks inauthorize.
Verification: go test ./pkg/httperr ./pkg/errcode ./pkg/i18n passed. go test ./modules/oidc -run 'TestOIDCBindNoLegacyResponseError|TestBindError_' passed. Full go test ./modules/oidc was blocked by local Redis/MySQL dependencies on 127.0.0.1:6379 and 127.0.0.1:3306, not by the changed tests.
lml2468
left a comment
There was a problem hiding this comment.
Summary
Clean, well-structured migration of the OIDC bind flow error responses from raw c.AbortWithStatusJSON(errMsg(...)) to the localized ResponseErrorLWithStatus envelope. No blocking issues.
What this PR does
pkg/httperr/respond.goβ AddsResponseErrorLWithStatusfacade that keeps the code's canonical HTTP status on the wire (vs legacy fixed-400). SharedrespondLimplementation avoids duplication. Clean design.pkg/errcode/oidc.goβ 12 OIDC bind error codes. Anti-enumeration: single 401bind_invalid_credentialsfor all auth failures.Internal=trueonly on 503 β correct.modules/oidc/api_i18n.goβrespondBindError+mustLookupSharedCodeinit-time resolution with panic guard. Correct: fail loudly at startup, not silently at request time.modules/oidc/api_bind.goβ ~30 call sites migrated. EveryAbortWithStatusJSONreplaced withrespondBindError.respondBindErrorcallsc.Abort()matching the priorAbortWithStatusJSONsemantics β no control-flow regression.modules/oidc/api.goβ Bonus: closeserr.Error()reflection leak in authorize endpoint. Generic messages + zap logging instead. Good security hygiene.- Tests β Comprehensive: code registration, Internal-flag invariant, anti-enumeration contract, source guard (no legacy patterns in api_bind.go), wire-status assertions, zh-CN translation check.
- i18n β Both locale files (zh-CN TOML, en-US marker) updated. 12/12 codes covered.
- Baseline β
api_bind.gocount correctly updated to 0 with EXEMPT annotation.
Findings
P2 (nit)
modules/oidc/api_i18n.go:26βcodeSharedAuthRequiredis declared but appears unused in this diff. If it's for a subsequent PR, a brief// used by api_bind_auth.go (next PR)comment would prevent dead-code confusion. If truly unused, consider removing.
Verdict
APPROVED. Architecture is clean, test coverage is thorough, anti-enumeration design is solid, no error-leak regressions.
β¦503 Internal - Remove unused codeSharedAuthRequired: logout's 401 stays a raw protocol endpoint (not migrated to the envelope), so the shared code was dead. - Document that ErrOIDCBindServiceUnavailable's Internal=true makes its specific message and zh-CN translation unreachable by design (5xx hygiene); they are kept only to satisfy Register and document intent.
|
Thanks for the reviews! Addressed both non-blocking findings in 6f7ac3e:
|
yujiawei
left a comment
There was a problem hiding this comment.
Re-review of new commit 6f7ac3e (chore: address review nits) β APPROVED
verdict: approve
head-sha: 6f7ac3e
repo: Mininglamp-OSS/octo-server
This re-review covers only the delta since the prior approval at ced0869 (this commit is +8/-3 across 2 files):
modules/oidc/api_i18n.goβ drops the now-unusedcodeSharedAuthRequiredvar. Verified genuinely dead: zero references remain inmodules/oidc; the shared codeerr.shared.auth.requiredstays registered inpkg/i18n/codes/shared.goand is still used bymodules/user|message|group. The two retained vars (codeSharedRateLimited,codeSharedInternal) are both still referenced inapi_bind.go.pkg/errcode/oidc.goβ doc-only comment onErrOIDCBindServiceUnavailableexplaining theInternal=true5xx-hygiene contract (specific message + zh-CN intentionally unreachable on the wire; clients distinguish the 503 viaerror.http_status). No behavior change; the code is still registered.
Local verification: go build + go vet clean on modules/oidc and pkg/errcode. Targeted tests pass: TestBindError_*, TestOIDCBindNoLegacyResponseError, TestAPI_BindCreate_NilBind_503, TestAPI_BindHandlers_NilBindReturnsServiceUnavailable, TestOIDCCodesRegistered/InternalFlag/InvalidCredentialsGeneric, and all of pkg/i18n/.... (The only failure is the pre-existing TestRedisBindStore_Behavior_Integration, which requires a live MySQL β unrelated to this change.) CI is green across Build/Lint/Vet/Test/i18n/CodeQL.
Anti-enumeration (generic 401) and the Internal-flag invariant from the prior review still hold. No P0/P1.
yujiawei
left a comment
There was a problem hiding this comment.
Code Review β PR #223 (octo-server)
Verdict: APPROVED β re-review of head 6f7ac3e. No blocking issues.
Migrates the OIDC self-service bind flow (modules/oidc/api_bind.go) to the i18n error envelope and closes the raw-error leaks in the authorize protocol endpoint.
What looks good
ResponseErrorLWithStatusdesign is sound. The body envelope is byte-for-byte identical toResponseErrorL; only the transport status differs (realHTTPStatusvs legacy compatibility 400). SharedrespondLimpl keeps the two facades in lockstep β no renderer change needed. Scoping it to the bind flow (a recent feature with no fixed-400 clients) is the right call, and the divergence from the D14 fleet behavior is clearly documented.- Security is handled carefully. Anti-enumeration is enforced by collapsing
ErrBindAuthRejected+ the confirm-stage TOCTOU rejection into a single genericErrOIDCBindInvalidCredentials(401), with the specific reason logged via zap only.oidc_test.goguards this with a message-leak assertion. - 5xx hygiene.
Internal=trueis asserted to be present on (and only on) 5xx codes; the 503 message/translation are intentionally unreachable and documented as such. Theauthorizeendpoint now logs internal errors and returns generic copy instead of echoingerr.Error()β good defense-in-depth against reflecting client input. - Test coverage is thorough. Source-guard test (
TestOIDCBindNoLegacyResponseError) prevents regressions back to rawc.AbortWithStatusJSON, plus real-renderer i18n contract tests for status preservation (410), validation (400), anti-enumeration (401), and zh-CN translation. errcode registration + Internal-flag invariants are covered. - i18n bookkeeping is complete. en-US and zh-CN entries added for all 12 codes; the lint baseline correctly drops
api_bind.goto 0 and documents theapi.goEXEMPT rationale.
Non-blocking notes
api.go(authorize/callback/logout) intentionally stays on rawerrMsg()as browser-facing protocol endpoints β consistent with the documented EXEMPT note and out of scope here.
CI is fully green (Build / Vet / Lint / Test / CodeQL / i18n checks all pass).
Jerry-Xin
left a comment
There was a problem hiding this comment.
β Re-Review: APPROVE
Delta since ced0869 β 6f7ac3e:
The new commits harden authorize() β all five err.Error() reflections are replaced with generic errMsg("internal error") + structured zap.Error(err) logging. A defense-in-depth comment on ValidateReturnTo explicitly calls out the echo-back risk.
Bind-flow migration (the bulk of the PR) is unchanged from the previous review.
No blocking issues. Previous non-blocking observations still apply:
- π‘
codeSharedAuthRequiredinapi_i18n.gois declared but unused in this diff - π‘
ErrOIDCBindServiceUnavailablehasInternal: true, making its localized message unreachable (acceptable for 5xx hygiene)
Clean delta, approve stands.
Summary
Migrates the OIDC self-service bind flow (
modules/oidc/api_bind.go) to the i18n error envelope and closes the raw-error leaks in theauthorizeprotocol endpoint. Part of the i18n backend rollout (#170).OIDC was outside the Phase 2 hotspot batch because it never used the
c.ResponseErrorenvelope β it emitted rawc.AbortWithStatusJSON(errMsg(...))with a private{"msg": ...}shape and semantic HTTP status codes (400/401/409/410/422/429/503).Closes #220.
Design decision β
ResponseErrorLWithStatus(keeps real HTTP status)The bind endpoints are a recent feature with no legacy clients depending on the fixed-400 compatibility behavior (D14). Pinning their 410/429/409/422/503 responses to 400 would be a regression (degraded semantics, broken status-based branching, extra churn at the Phase 4 switch).
This PR adds a second facade in
pkg/httperr:ResponseErrorL(unchanged): wire status pinned to 400, real status inerror.http_status(D14) β used by all legacy-bearing modules.ResponseErrorLWithStatus(new): wire status = the code's canonicalHTTPStatus. Body envelope is byte-for-byte identical; only the transport status differs. The renderer already readsspec.TransportStatus, so there is zero renderer change.This is the only consumer for now; it effectively brings the bind endpoints to their Phase 4 end state early. Flagged for maintainer review as it diverges from the current D14 fleet-wide consistency.
What changed
feat(i18n):ResponseErrorLWithStatusfacade (+ sharedrespondL).feat(i18n): 12err.server.oidc.*codes + zh-CN translations (reuses sharedrate.limited/internal/auth.required).feat(i18n): migrate all 43 raw sites inapi_bind.goviarespondBindError.fix(oidc): stop leakingerr.Error()inauthorize(5 sites β generic message +zap.Error).test(i18n): source guard + i18n contract coverage.Preserved contracts
ErrBindAuthRejectedand the confirm-stage TOCTOU rejection all map to a single genericbind_invalid_credentials(401); the specific reason is logged viazaponly. Test-locked.writeAudit/bindResultFromErrside effect is kept; only the final response call changed.Internal=true; clients identify the transient 503 viaerror.http_status.Breaking-change assessment
HTTP status codes: zero drift. Per-status-bucket counts are identical before/after (
400Γ12 / 401Γ3 / 409Γ6 / 410Γ5 / 422Γ1 / 429Γ4 / 500Γ5 / 503Γ1), and the existing 30 status-code assertions inapi_bind_test.goall pass post-migration. Success responses (c.JSON(200)) are untouched.callback/logoutare untouched.Bind response body (incremental):
{"msg": "..."}β{"msg": "<i18n>", "status": <real>, "error": {code, message, http_status, details}}. Newstatus/errorfields are additive (JSON-compatible); themsgvalue changes (short phrase β full localized sentence). AddsContent-Language+Varyheaders (already in CORS ExposeHeaders).msgto branch error handling. The correct approach is to read the HTTP status (unchanged) orerror.code(new). Please confirm the bind wizard does not string-matchmsg.Test plan
go build ./...,go vet,gofmt(changed files clean)make i18n-extract-check(recall 100%),make i18n-lintgo test ./modules/oidc -race(full suite, fresh test DB),pkg/httperr/pkg/errcode/pkg/i18nerror.code(notmsgstring match)