mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 14:37:34 +00:00
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:
@@ -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
8
go.mod
@@ -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
18
go.sum
@@ -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=
|
||||||
|
@@ -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.
|
||||||
|
98
internal/api/oidc/auth_request_test.go
Normal file
98
internal/api/oidc/auth_request_test.go
Normal 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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -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())
|
||||||
|
|
||||||
|
@@ -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())
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user