mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-22 23:42:16 +00:00
Merge commit from fork
* fix: Improve host header handling and deployment mode detection * ZITADEL_CLOUD env * refactor service config * service config * cleanup * cleanup * util for original and public host * instanceHost, publicHost * cleanup constructUrl * cleanup middleware * add x-zitadel-forward-host as fallback for current cloud setup * fix build * fix: lint * fix: update test mocks * fix: provide basepath to unit tests * host handling * unit tests * imports * fix: getPublicHost instead of getInstanceHost * unit tests * update nextjs, implement jwt verify for logout_token * pass logout information in jwt to login v2 * handle hosts in jwks url * point idp callbacks to the api and not the login UI * logout_hint out of logout token, typecheck * rm unused imports * fix oidc integration test --------- Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
co-authored by
Livio Spring
parent
f9e3871bb3
commit
df75be96ff
@@ -10,10 +10,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
@@ -33,6 +34,7 @@ const (
|
||||
LoginPostLogoutRedirectParam = "post_logout_redirect"
|
||||
LoginLogoutHintParam = "logout_hint"
|
||||
LoginUILocalesParam = "ui_locales"
|
||||
LoginLogoutTokenParam = "logout_token"
|
||||
LoginPath = "/login"
|
||||
LogoutPath = "/logout"
|
||||
LogoutDonePath = "/logout/done"
|
||||
@@ -298,7 +300,11 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
|
||||
} else {
|
||||
logoutURI = logoutURI.JoinPath(LogoutPath)
|
||||
}
|
||||
return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales), nil
|
||||
signer, _, err := GetSignerOnce(o.query.GetActiveSigningWebKey)(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales, signer)
|
||||
}
|
||||
|
||||
// V1:
|
||||
@@ -375,7 +381,13 @@ func (o *OPStorage) federatedLogout(ctx context.Context, sessionID string, postL
|
||||
return login.ExternalLogoutPath(sessionID)
|
||||
}
|
||||
|
||||
func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales []language.Tag) string {
|
||||
type logoutTokenPayload struct {
|
||||
PostLogoutRedirectURI string `json:"post_logout_redirect_uri,omitempty"`
|
||||
LogoutHint string `json:"logout_hint,omitempty"`
|
||||
UILocales oidc.Locales `json:"ui_locales,omitempty"`
|
||||
}
|
||||
|
||||
func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales oidc.Locales, signer jose.Signer) (string, error) {
|
||||
if strings.HasSuffix(logoutURI.Path, "/") && len(logoutURI.Path) > 1 {
|
||||
logoutURI.Path = strings.TrimSuffix(logoutURI.Path, "/")
|
||||
}
|
||||
@@ -386,14 +398,19 @@ func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, u
|
||||
q.Set(LoginLogoutHintParam, logoutHint)
|
||||
}
|
||||
if len(uiLocales) > 0 {
|
||||
locales := make([]string, len(uiLocales))
|
||||
for i, locale := range uiLocales {
|
||||
locales[i] = locale.String()
|
||||
}
|
||||
q.Set(LoginUILocalesParam, strings.Join(locales, " "))
|
||||
q.Set(LoginUILocalesParam, uiLocales.String())
|
||||
}
|
||||
logoutToken, err := crypto.Sign(&logoutTokenPayload{
|
||||
PostLogoutRedirectURI: redirectURI,
|
||||
LogoutHint: logoutHint,
|
||||
UILocales: uiLocales,
|
||||
}, signer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
q.Set(LoginLogoutTokenParam, logoutToken)
|
||||
logoutURI.RawQuery = q.Encode()
|
||||
return logoutURI.String()
|
||||
return logoutURI.String(), nil
|
||||
}
|
||||
|
||||
// v2PostLogoutRedirectURI will take care that the post_logout_redirect_uri is correctly set for v2 logins.
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
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"
|
||||
@@ -18,6 +22,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
redirectURI string
|
||||
logoutHint string
|
||||
uiLocales []language.Tag
|
||||
signer jose.Signer
|
||||
expectedParams map[string]string
|
||||
}{
|
||||
{
|
||||
@@ -26,6 +31,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
redirectURI: "https://client/cb",
|
||||
expectedParams: map[string]string{
|
||||
"post_logout_redirect": "https://client/cb",
|
||||
"logout_token": "", // presence checked separately
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -36,6 +42,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
expectedParams: map[string]string{
|
||||
"post_logout_redirect": "https://client/cb",
|
||||
"logout_hint": "[email protected]",
|
||||
"logout_token": "", // presence checked separately
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -46,6 +53,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
expectedParams: map[string]string{
|
||||
"post_logout_redirect": "https://client/cb",
|
||||
"ui_locales": "en it",
|
||||
"logout_token": "", // presence checked separately
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -58,6 +66,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
"post_logout_redirect": "https://client/cb",
|
||||
"logout_hint": "logoutme",
|
||||
"ui_locales": "de-CH fr",
|
||||
"logout_token": "", // presence checked separately
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -66,6 +75,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
redirectURI: "https://client/cb",
|
||||
expectedParams: map[string]string{
|
||||
"post_logout_redirect": "https://client/cb",
|
||||
"logout_token": "", // presence checked separately
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -79,9 +89,10 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// When
|
||||
got := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales)
|
||||
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/")
|
||||
@@ -91,8 +102,39 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
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
|
||||
}()
|
||||
)
|
||||
|
||||
@@ -551,15 +551,16 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func buildLogoutURL(origin, logoutURLV2 string, redirectURI string, extraParams map[string]string) string {
|
||||
func buildLogoutURL(origin, logoutURLV2, redirectURI string, extraParams map[string]string) *url.URL {
|
||||
u, _ := url.Parse(origin + logoutURLV2 + redirectURI)
|
||||
q := u.Query()
|
||||
for k, v := range extraParams {
|
||||
q.Set(k, v)
|
||||
}
|
||||
q.Set("logout_token", "signed-logout-token") // placeholder
|
||||
u.RawQuery = q.Encode()
|
||||
// Append the redirect URI as a URL-escaped string
|
||||
return u.String()
|
||||
return u
|
||||
}
|
||||
|
||||
func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
|
||||
@@ -567,7 +568,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
|
||||
name string
|
||||
clientID string
|
||||
authRequestID func(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string
|
||||
logoutURL string
|
||||
logoutURL *url.URL
|
||||
}{
|
||||
{
|
||||
name: "login header",
|
||||
@@ -614,7 +615,18 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
|
||||
|
||||
postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state", "hint", oidc.ParseLocales([]string{"it-IT", "en-US"}))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.logoutURL, postLogoutRedirect.String())
|
||||
|
||||
requiredQueries := tt.logoutURL.Query()
|
||||
for key, value := range requiredQueries {
|
||||
if key == "logout_token" {
|
||||
assert.NotEmpty(t, value[0], "logout_token must be present")
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, value[0], postLogoutRedirect.Query().Get(key))
|
||||
}
|
||||
requiredURLWithoutQueries := *tt.logoutURL
|
||||
requiredURLWithoutQueries.RawQuery = ""
|
||||
assert.Equal(t, requiredURLWithoutQueries.String(), postLogoutRedirect.Scheme+"://"+postLogoutRedirect.Host+postLogoutRedirect.Path)
|
||||
|
||||
// userinfo must not fail until login UI terminated session
|
||||
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
||||
|
||||
Reference in New Issue
Block a user