feat(OIDC): handle logout hint on end_session_endpoint (#10039)

# Which Problems Are Solved

The OIDC session endpoint allows to pass a `id_token_hint` to identify
the session to terminate. In case the application is not able to pass
that, e.g. Console currently allows multiple sessions to be open, but
will only store the id_token of the current session, allowing to pass
the `logout_hint` to identify the user adds some new possibilities.

# How the Problems Are Solved

In case the end_session_endpoint is called with no `id_token_hint`, but
a `logout_hint` and the v2 login UI is configured, the information is
passed to the login UI also as `login_hint` parameter to allow the login
UI to determine the session to be terminated, resp. let the user decide.

# Additional Changes

Also added the `ui_locales` as parameter to handle and pass to the V2
login UI.

# Dependencies ⚠️ 

~These changes depend on https://github.com/zitadel/oidc/pull/774~

# Additional Context

closes #9847

---------

Co-authored-by: Marco Ardizzone <marco@zitadel.com>
This commit is contained in:
Livio Spring
2025-07-28 09:55:55 -04:00
committed by GitHub
parent e4f633bcb3
commit 5d2d1d6da6
7 changed files with 169 additions and 35 deletions

View File

@@ -656,12 +656,14 @@ The endpoint has to be opened in the user agent (browser) to terminate the user
No parameters are needed apart from the user agent cookie, but you can provide the following to customize the behavior: No parameters are needed apart from the user agent cookie, but you can provide the following to customize the behavior:
| Parameter | Description | | Parameter | Description |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id_token_hint | the id_token that was previously issued to the client | | id_token_hint | the id_token that was previously issued to the client |
| client_id | client_id of the application | | client_id | client_id of the application |
| post_logout_redirect_uri | Callback uri of the logout where the user (agent) will be redirected to. Must match exactly one of the preregistered in Console. | | post_logout_redirect_uri | Callback uri of the logout where the user (agent) will be redirected to. Must match exactly one of the preregistered in Console. |
| state | Opaque value used to maintain state between the request and the callback | | state | Opaque value used to maintain state between the request and the callback |
| logout_hint | A valid login name of a user. Will be used to select the user to logout. Only supported when using the login UI V2. |
| ui_locales | Spaces delimited list of preferred locales for the login UI, e.g. `de-CH de en`. If none is provided or matches the possible locales provided by the login UI, the `accept-language` header of the browser will be taken into account. |
The `post_logout_redirect_uri` will be checked against the previously registered uris of the client provided by the `azp` claim of the `id_token_hint` or the `client_id` parameter. The `post_logout_redirect_uri` will be checked against the previously registered uris of the client provided by the `azp` claim of the `id_token_hint` or the `client_id` parameter.
If both parameters are provided, they must be equal. If both parameters are provided, they must be equal.

8
go.mod
View File

@@ -82,7 +82,7 @@ require (
github.com/twilio/twilio-go v1.26.1 github.com/twilio/twilio-go v1.26.1
github.com/zitadel/exifremove v0.1.0 github.com/zitadel/exifremove v0.1.0
github.com/zitadel/logging v0.6.2 github.com/zitadel/logging v0.6.2
github.com/zitadel/oidc/v3 v3.39.1 github.com/zitadel/oidc/v3 v3.42.0
github.com/zitadel/passwap v0.9.0 github.com/zitadel/passwap v0.9.0
github.com/zitadel/saml v0.3.5 github.com/zitadel/saml v0.3.5
github.com/zitadel/schema v1.3.1 github.com/zitadel/schema v1.3.1
@@ -101,8 +101,8 @@ require (
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
golang.org/x/net v0.40.0 golang.org/x/net v0.40.0
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.15.0 golang.org/x/sync v0.16.0
golang.org/x/text v0.26.0 golang.org/x/text v0.27.0
google.golang.org/api v0.233.0 google.golang.org/api v0.233.0
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9
google.golang.org/grpc v1.72.1 google.golang.org/grpc v1.72.1
@@ -119,7 +119,7 @@ require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/bmatcuk/doublestar/v4 v4.9.0 // indirect
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
github.com/crewjam/httperr v0.2.0 // indirect github.com/crewjam/httperr v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect

18
go.sum
View File

@@ -104,8 +104,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.9.0 h1:DBvuZxjdKkRP/dr4GVV4w2fnmrk5Hxc90T51LZjv0JA=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.9.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -810,8 +810,10 @@ github.com/zitadel/exifremove v0.1.0 h1:qD50ezWsfeeqfcvs79QyyjVfK+snN12v0U0deaU8
github.com/zitadel/exifremove v0.1.0/go.mod h1:rzKJ3woL/Rz2KthVBiSBKIBptNTvgmk9PLaeUKTm+ek= github.com/zitadel/exifremove v0.1.0/go.mod h1:rzKJ3woL/Rz2KthVBiSBKIBptNTvgmk9PLaeUKTm+ek=
github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU=
github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4=
github.com/zitadel/oidc/v3 v3.39.1 h1:6QwGwI3yxh4somT7fwRCeT1KOn/HOGv0PA0dFciwJjE= github.com/zitadel/oidc/v3 v3.41.1-0.20250718152526-16ebef905b40 h1:MmUhfhwIcPStWqsTW+Pw+kYa5SNY7TxwzktUDohwO78=
github.com/zitadel/oidc/v3 v3.39.1/go.mod h1:aH8brOrzoliAybVdfq2xIdGvbtl0j/VsKRNa7WE72gI= github.com/zitadel/oidc/v3 v3.41.1-0.20250718152526-16ebef905b40/go.mod h1:Y/rY7mHTzMGrZgf7REgQZFWxySlaSVqqFdBmNZq+9wA=
github.com/zitadel/oidc/v3 v3.42.0 h1:cqlCYIEapmDprp5a5hUl9ivkUOu1SQxOqbrKdalHqGk=
github.com/zitadel/oidc/v3 v3.42.0/go.mod h1:Y/rY7mHTzMGrZgf7REgQZFWxySlaSVqqFdBmNZq+9wA=
github.com/zitadel/passwap v0.9.0 h1:QvDK8OHKdb73C0m+mwXvu87UJSBqix3oFwTVENHdv80= github.com/zitadel/passwap v0.9.0 h1:QvDK8OHKdb73C0m+mwXvu87UJSBqix3oFwTVENHdv80=
github.com/zitadel/passwap v0.9.0/go.mod h1:6QzwFjDkIr3FfudzSogTOx5Ydhq4046dRJtDM/kX+G8= github.com/zitadel/passwap v0.9.0/go.mod h1:6QzwFjDkIr3FfudzSogTOx5Ydhq4046dRJtDM/kX+G8=
github.com/zitadel/saml v0.3.5 h1:L1RKWS5y66cGepVxUGjx/WSBOtrtSpRA/J3nn5BJLOY= github.com/zitadel/saml v0.3.5 h1:L1RKWS5y66cGepVxUGjx/WSBOtrtSpRA/J3nn5BJLOY=
@@ -948,8 +950,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -992,8 +994,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=

View File

@@ -13,6 +13,7 @@ import (
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/oidc/v3/pkg/op"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http" http_utils "github.com/zitadel/zitadel/internal/api/http"
@@ -30,6 +31,8 @@ import (
const ( const (
LoginClientHeader = "x-zitadel-login-client" LoginClientHeader = "x-zitadel-login-client"
LoginPostLogoutRedirectParam = "post_logout_redirect" LoginPostLogoutRedirectParam = "post_logout_redirect"
LoginLogoutHintParam = "logout_hint"
LoginUILocalesParam = "ui_locales"
LoginPath = "/login" LoginPath = "/login"
LogoutPath = "/logout" LogoutPath = "/logout"
LogoutDonePath = "/logout/done" LogoutDonePath = "/logout/done"
@@ -283,14 +286,19 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
// we'll redirect to the UI (V2) and let it decide which session to terminate // we'll redirect to the UI (V2) and let it decide which session to terminate
// //
// If there's no id_token_hint and for v1 logins, we handle them separately // If there's no id_token_hint and for v1 logins, we handle them separately
if endSessionRequest.IDTokenHintClaims == nil && if endSessionRequest.IDTokenHintClaims == nil && (authz.GetFeatures(ctx).LoginV2.Required || headers.Get(LoginClientHeader) != "") {
(authz.GetFeatures(ctx).LoginV2.Required || headers.Get(LoginClientHeader) != "") {
redirectURI := v2PostLogoutRedirectURI(endSessionRequest.RedirectURI) redirectURI := v2PostLogoutRedirectURI(endSessionRequest.RedirectURI)
// if no base uri is set, fallback to the default configured in the runtime config logoutURI := authz.GetFeatures(ctx).LoginV2.BaseURI
if authz.GetFeatures(ctx).LoginV2.BaseURI == nil || authz.GetFeatures(ctx).LoginV2.BaseURI.String() == "" { // if no logout uri is set, fallback to the default configured in the runtime config
return o.defaultLogoutURLV2 + redirectURI, nil if logoutURI == nil || logoutURI.String() == "" {
logoutURI, err = url.Parse(o.defaultLogoutURLV2)
if err != nil {
return "", err
}
} else {
logoutURI = logoutURI.JoinPath(LogoutPath)
} }
return buildLoginV2LogoutURL(authz.GetFeatures(ctx).LoginV2.BaseURI, redirectURI), nil return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales), nil
} }
// V1: // V1:
@@ -367,12 +375,25 @@ func (o *OPStorage) federatedLogout(ctx context.Context, sessionID string, postL
return login.ExternalLogoutPath(sessionID) return login.ExternalLogoutPath(sessionID)
} }
func buildLoginV2LogoutURL(baseURI *url.URL, redirectURI string) string { func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales []language.Tag) string {
baseURI.JoinPath(LogoutPath) if strings.HasSuffix(logoutURI.Path, "/") && len(logoutURI.Path) > 1 {
q := baseURI.Query() logoutURI.Path = strings.TrimSuffix(logoutURI.Path, "/")
}
q := logoutURI.Query()
q.Set(LoginPostLogoutRedirectParam, redirectURI) q.Set(LoginPostLogoutRedirectParam, redirectURI)
baseURI.RawQuery = q.Encode() if logoutHint != "" {
return baseURI.String() 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, " "))
}
logoutURI.RawQuery = q.Encode()
return logoutURI.String()
} }
// v2PostLogoutRedirectURI will take care that the post_logout_redirect_uri is correctly set for v2 logins. // v2PostLogoutRedirectURI will take care that the post_logout_redirect_uri is correctly set for v2 logins.

View File

@@ -0,0 +1,98 @@
package oidc
import (
"net/url"
"testing"
"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
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",
},
},
{
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",
},
},
{
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",
},
},
{
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",
},
},
{
testName: "base with trailing slash",
logoutURIStr: "https://example.com/logout/",
redirectURI: "https://client/cb",
expectedParams: map[string]string{
"post_logout_redirect": "https://client/cb",
},
},
}
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 := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales)
// Then
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 {
assert.Equal(t, v, q.Get(k))
}
})
}
}

View File

@@ -498,7 +498,7 @@ func TestOPStorage_TerminateSession(t *testing.T) {
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
require.NoError(t, err) require.NoError(t, err)
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state", "", nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())
@@ -535,7 +535,7 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
require.NoError(t, err) require.NoError(t, err)
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state", "", nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())
@@ -551,6 +551,17 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
func buildLogoutURL(origin, logoutURLV2 string, redirectURI string, extraParams map[string]string) string {
u, _ := url.Parse(origin + logoutURLV2 + redirectURI)
q := u.Query()
for k, v := range extraParams {
q.Set(k, v)
}
u.RawQuery = q.Encode()
// Append the redirect URI as a URL-escaped string
return u.String()
}
func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -565,7 +576,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
return clientID return clientID
}(), }(),
authRequestID: createAuthRequest, authRequestID: createAuthRequest,
logoutURL: http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure) + Instance.Config.LogoutURLV2 + logoutRedirectURI + "?state=state", logoutURL: buildLogoutURL(http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure), Instance.Config.LogoutURLV2, logoutRedirectURI+"?state=state", map[string]string{"logout_hint": "hint", "ui_locales": "it-IT en-US"}),
}, },
{ {
name: "login v2 config", name: "login v2 config",
@@ -574,7 +585,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
return clientID return clientID
}(), }(),
authRequestID: createAuthRequestNoLoginClientHeader, authRequestID: createAuthRequestNoLoginClientHeader,
logoutURL: http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure) + Instance.Config.LogoutURLV2 + logoutRedirectURI + "?state=state", logoutURL: buildLogoutURL(http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure), Instance.Config.LogoutURLV2, logoutRedirectURI+"?state=state", map[string]string{"logout_hint": "hint", "ui_locales": "it-IT en-US"}),
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@@ -601,7 +612,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
assertTokens(t, tokens, false) assertTokens(t, tokens, false)
assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID) assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state") postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state", "hint", oidc.ParseLocales([]string{"it-IT", "en-US"}))
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tt.logoutURL, postLogoutRedirect.String()) assert.Equal(t, tt.logoutURL, postLogoutRedirect.String())

View File

@@ -311,7 +311,7 @@ func Test_ZITADEL_API_terminated_session(t *testing.T) {
require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId()) require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
// end session // end session
postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state", "", nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())