mirror of
https://github.com/zitadel/zitadel.git
synced 2026-10-25 23:50:11 -05:00
fix(console): derive environment.json api from the requested origin (#12893)
<!-- Please inform yourself about the contribution guidelines on submitting a PR here: https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr. Take note of how PR/commit titles should be written and replace the template texts in the sections below. Don't remove any of the sections. It is important that the commit history clearly shows what is changed and why. Important: By submitting a contribution you agree to the terms from our Licensing Policy as described here: https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions. --> # Which Problems Are Solved - When an instance has multiple custom domains (or runs behind a reverse proxy that forwards the original host), the `api` attribute of the management console's `/ui/console/assets/environment.json` does not match the requested domain, while `issuer` does. - The `api` URL was built from the raw `r.Host` of the request, whereas the issuer is derived from the domain context that the `WithOrigin` middleware resolves from the instance / public host headers. # How the Problems Are Solved - The `api` URL is now taken from `http_util.DomainContext(ctx).Origin()`, the same source the OIDC issuer uses, so both values always match. # Additional Changes - Removes the now unused `externalSecure` parameter from `console.Start`. The protocol is already part of the domain context (`WithOrigin` is configured with `ExternalSecure`). - Adds a unit test for the `environment.json` handler covering the instance host, a public host and a forwarded instance host that differ from the request host. The test fails on `main` (`api` is `https://zitadel.internal:8080` instead of `https://custom.example.com`) and passes with this change. # Additional Context - Closes #12009 - Closes #12195 - AI-assisted: this change was prepared with Claude Code and reviewed and tested by me (`go test ./internal/api/ui/console/`, `go vet`, `go build ./cmd/...`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Livio Spring <9405495+livio-a@users.noreply.github.com>
This commit is contained in:
3 files changed
+86
-3
No files matched your search
+1
-1
@@ -727,7 +727,7 @@ func startAPIs(
|
||||
instanceInterceptor.HandlerFuncWithError,
|
||||
middleware.AuthorizationInterceptor(verifier, config.SystemAuthZ, config.InternalAuthZ).HandlerFuncWithError(schemas.HandlerPrefix)))
|
||||
|
||||
c, err := console.Start(config.Console, config.ExternalSecure, oidcServer.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal)
|
||||
c, err := console.Start(config.Console, oidcServer.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start management console: %w", err)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func (f *file) Stat() (_ fs.FileInfo, err error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, callDurationInterceptor, instanceHandler func(http.Handler) http.Handler, limitingAccessInterceptor *middleware.AccessInterceptor, customerPortal string) (http.Handler, error) {
|
||||
func Start(config Config, issuer op.IssuerFromRequest, callDurationInterceptor, instanceHandler func(http.Handler) http.Handler, limitingAccessInterceptor *middleware.AccessInterceptor, customerPortal string) (http.Handler, error) {
|
||||
fSys, err := fs.Sub(static, "static")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -122,7 +122,7 @@ func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, call
|
||||
instanceHandler,
|
||||
)
|
||||
env.HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
|
||||
url := http_util.BuildOrigin(r.Host, externalSecure)
|
||||
url := http_util.DomainContext(r.Context()).Origin()
|
||||
ctx := r.Context()
|
||||
instance := authz.GetInstance(ctx)
|
||||
instanceMgmtURL, err := templateInstanceManagementURL(config.InstanceManagementURL, instance)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
"github.com/zitadel/zitadel/internal/logstore/record"
|
||||
)
|
||||
|
||||
func TestStart_environmentJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
externalSecure bool
|
||||
requestHost string
|
||||
headers map[string]string
|
||||
wantAPI string
|
||||
}{
|
||||
{
|
||||
name: "request host",
|
||||
externalSecure: true,
|
||||
requestHost: "zitadel.example.com",
|
||||
wantAPI: "https://zitadel.example.com",
|
||||
},
|
||||
{
|
||||
name: "x-forwarded-host differs from request host",
|
||||
requestHost: "zitadel.internal:8080",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-Host": "custom.example.com",
|
||||
},
|
||||
wantAPI: "http://custom.example.com",
|
||||
},
|
||||
{
|
||||
name: "forwarded header differs from request host",
|
||||
requestHost: "zitadel.internal:8080",
|
||||
headers: map[string]string{
|
||||
"Forwarded": "host=custom.example.com;proto=https",
|
||||
},
|
||||
wantAPI: "https://custom.example.com",
|
||||
},
|
||||
{
|
||||
name: "public host differs from instance host",
|
||||
externalSecure: true,
|
||||
requestHost: "zitadel.internal:8080",
|
||||
headers: map[string]string{
|
||||
"x-zitadel-instance-host": "zitadel.example.com",
|
||||
"x-zitadel-public-host": "custom.example.com",
|
||||
},
|
||||
wantAPI: "https://custom.example.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
passthrough := func(next http.Handler) http.Handler { return next }
|
||||
issuer := func(r *http.Request) string { return "" }
|
||||
accessInterceptor := middleware.NewAccessInterceptor(&logstore.Service[*record.AccessLog]{}, nil, &middleware.AccessConfig{})
|
||||
handler, err := Start(Config{}, issuer, passthrough, passthrough, accessInterceptor, "")
|
||||
require.NoError(t, err)
|
||||
handler = middleware.WithOrigin(tt.externalSecure, "host", ":authority", []string{"x-zitadel-instance-host"}, []string{"x-zitadel-public-host"})(handler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, envRequestPath, nil)
|
||||
req.Host = tt.requestHost
|
||||
for key, value := range tt.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
|
||||
|
||||
var env struct {
|
||||
API string `json:"api"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &env))
|
||||
assert.Equal(t, tt.wantAPI, env.API)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user