mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-27 00:36:27 +00:00
This PR refactors the host resolution logic to establish a single source of truth for determining the instance and public hosts from request headers. It also ensures that headers are properly forwarded to APIs for multi-tenant routing.
Centralized Host Resolution (host.ts)
Created dedicated functions in `src/lib/server/host.ts` to handle host resolution:
1. `getInstanceHost(headers)`: Returns the instance host used for API routing
Priority: x-zitadel-instance-host → x-forwarded-host → host
Used for determining which ZITADEL instance to route API calls to
2. `getPublicHost(headers)`: Returns the public-facing host that users see
Priority: x-forwarded-host → host (explicitly excludes x-zitadel-instance-host)
Used for generating user-facing URLs (password reset links, etc.)
Additionally, on logout / end_session the parameters are passed as a JWT to safely pass the state between the API and the login UI V2.
---------
Co-authored-by: Livio Spring <livio.a@gmail.com>
(cherry picked from commit df75be96ff)
141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
package oidc
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/json"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/go-jose/go-jose/v4"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
func TestBuildLoginV2LogoutURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tt := []struct {
|
|
testName string
|
|
logoutURIStr string
|
|
redirectURI string
|
|
logoutHint string
|
|
uiLocales []language.Tag
|
|
signer jose.Signer
|
|
expectedParams map[string]string
|
|
}{
|
|
{
|
|
testName: "basic with only redirectURI",
|
|
logoutURIStr: "https://example.com/logout",
|
|
redirectURI: "https://client/cb",
|
|
expectedParams: map[string]string{
|
|
"post_logout_redirect": "https://client/cb",
|
|
"logout_token": "", // presence checked separately
|
|
},
|
|
},
|
|
{
|
|
testName: "with logout hint",
|
|
logoutURIStr: "https://example.com/logout",
|
|
redirectURI: "https://client/cb",
|
|
logoutHint: "user@example.com",
|
|
expectedParams: map[string]string{
|
|
"post_logout_redirect": "https://client/cb",
|
|
"logout_hint": "user@example.com",
|
|
"logout_token": "", // presence checked separately
|
|
},
|
|
},
|
|
{
|
|
testName: "with ui_locales",
|
|
logoutURIStr: "https://example.com/logout",
|
|
redirectURI: "https://client/cb",
|
|
uiLocales: []language.Tag{language.English, language.Italian},
|
|
expectedParams: map[string]string{
|
|
"post_logout_redirect": "https://client/cb",
|
|
"ui_locales": "en it",
|
|
"logout_token": "", // presence checked separately
|
|
},
|
|
},
|
|
{
|
|
testName: "with all params",
|
|
logoutURIStr: "https://example.com/logout",
|
|
redirectURI: "https://client/cb",
|
|
logoutHint: "logoutme",
|
|
uiLocales: []language.Tag{language.Make("de-CH"), language.Make("fr")},
|
|
expectedParams: map[string]string{
|
|
"post_logout_redirect": "https://client/cb",
|
|
"logout_hint": "logoutme",
|
|
"ui_locales": "de-CH fr",
|
|
"logout_token": "", // presence checked separately
|
|
},
|
|
},
|
|
{
|
|
testName: "base with trailing slash",
|
|
logoutURIStr: "https://example.com/logout/",
|
|
redirectURI: "https://client/cb",
|
|
expectedParams: map[string]string{
|
|
"post_logout_redirect": "https://client/cb",
|
|
"logout_token": "", // presence checked separately
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.testName, func(t *testing.T) {
|
|
// t.Parallel()
|
|
|
|
// Given
|
|
logoutURI, err := url.Parse(tc.logoutURIStr)
|
|
require.NoError(t, err)
|
|
|
|
// When
|
|
got, err := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales, signer)
|
|
|
|
// Then
|
|
require.NoError(t, err)
|
|
gotURL, err := url.Parse(got)
|
|
require.NoError(t, err)
|
|
require.NotContains(t, gotURL.String(), "/logout/")
|
|
|
|
q := gotURL.Query()
|
|
// Ensure no unexpected params
|
|
require.Len(t, q, len(tc.expectedParams))
|
|
|
|
for k, v := range tc.expectedParams {
|
|
if k == LoginLogoutTokenParam {
|
|
assertLogoutToken(t, q.Get(k), &logoutTokenPayload{
|
|
PostLogoutRedirectURI: tc.redirectURI,
|
|
LogoutHint: tc.logoutHint,
|
|
UILocales: tc.uiLocales,
|
|
})
|
|
continue
|
|
}
|
|
assert.Equal(t, v, q.Get(k))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertLogoutToken(t *testing.T, token string, payload *logoutTokenPayload) {
|
|
signature, err := jose.ParseSigned(token, []jose.SignatureAlgorithm{jose.RS256})
|
|
require.NoError(t, err)
|
|
logoutToken := new(logoutTokenPayload)
|
|
err = json.Unmarshal(signature.UnsafePayloadWithoutVerification(), logoutToken)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, payload, logoutToken)
|
|
}
|
|
|
|
var (
|
|
privKey, _ = rsa.GenerateKey(rand.Reader, 2048)
|
|
signer = func() jose.Signer {
|
|
signer, _ := jose.NewSigner(
|
|
jose.SigningKey{
|
|
Algorithm: jose.RS256,
|
|
Key: privKey,
|
|
},
|
|
(&jose.SignerOptions{}).WithType("JWT"),
|
|
)
|
|
return signer
|
|
}()
|
|
)
|