From c7b4e90106478474bf0639320493326b769779b1 Mon Sep 17 00:00:00 2001 From: Mathieu Fenniak Date: Wed, 11 Mar 2026 04:00:09 +0100 Subject: [PATCH] [v14.0/forgejo]: fix: remove second challenge from WWW-Authenticate header (#11625) **Backport**: #11616 https://codeberg.org/forgejo/forgejo/pulls/11393 introduced a second challenge, one for HTTP Basic Authentication, to the existing `WWW-Authenticate` header sent by Forgejo's container registry in response to missing or invalid credentials. However, that led to unexpected compatibility issues with some clients. For example, it broke Renovate (see https://github.com/renovatebot/renovate/discussions/41774). To be extra-safe, the decision was taken to revert that particular change without introducing a second header field (i.e., sending two `WWW-Authenticate` headers). That effectively restores the old behaviour. ``` $ curl -v -u andreas --basic http://192.168.178.62:3000/v2 Enter host password for user 'andreas': * Trying 192.168.178.62:3000... * Connected to 192.168.178.62 (192.168.178.62) port 3000 * using HTTP/1.x * Server auth using Basic with user 'andreas' > GET /v2 HTTP/1.1 > Host: 192.168.178.62:3000 > Authorization: Basic ***** > User-Agent: curl/8.15.0 > Accept: */* > * Request completely sent off < HTTP/1.1 401 Unauthorized < Content-Length: 50 < Content-Type: application/json < Docker-Distribution-Api-Version: registry/2.0 < Www-Authenticate: Bearer realm="http://192.168.178.62:3000/v2/token",service="container_registry",scope="*" < Date: Tue, 10 Mar 2026 17:00:21 GMT < {"errors":[{"code":"UNAUTHORIZED","message":""}]} ``` ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes (can be removed for JavaScript changes) - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Tests for JavaScript changes (can be removed for Go changes) - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. *The decision if the pull request will be shown in the release notes is up to the mergers / release team.* The content of the `release-notes/.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead. (cherry picked from commit f1a08a7ab17fe7523a111fb127419b9e743f48d1) Co-authored-by: Andreas Ahlenstorf Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11625 Reviewed-by: Gusted --- routers/api/packages/container/container.go | 5 +++-- .../api_packages_container_cleanup_sha256_test.go | 3 +-- tests/integration/api_packages_container_test.go | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 373847b3ab..8c967d8472 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -126,8 +126,9 @@ func apiErrorDefined(ctx *context.Context, err *namedError) { } func APIUnauthorizedError(ctx *context.Context) { - ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*",`+ - `Basic realm="`+setting.AppURL+`v2",service="container_registry",scope="*"`) + // Do not include more than one challenge in the same header field. That breaks clients even though the HTTP RFC + // allows it. + ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`) apiErrorDefined(ctx, errUnauthorized) } diff --git a/tests/integration/api_packages_container_cleanup_sha256_test.go b/tests/integration/api_packages_container_cleanup_sha256_test.go index d672504509..19b73f7698 100644 --- a/tests/integration/api_packages_container_cleanup_sha256_test.go +++ b/tests/integration/api_packages_container_cleanup_sha256_test.go @@ -63,8 +63,7 @@ func TestPackageContainerCleanupSHA256(t *testing.T) { Token string `json:"token"` } - authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*",` + - `Basic realm="` + setting.AppURL + `v2",service="container_registry",scope="*"`} + authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`} t.Run("User", func(t *testing.T) { req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)) diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index 9143ff973b..c688679a67 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -86,8 +86,7 @@ func TestPackageContainer(t *testing.T) { Token string `json:"token"` } - authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*",` + - `Basic realm="` + setting.AppURL + `v2",service="container_registry",scope="*"`} + authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`} t.Run("Anonymous", func(t *testing.T) { defer tests.PrintCurrentTest(t)()