Skip to content

Commit 8282aef

Browse files
authored
feat(auth): support OIDC RP-Initiated Logout (#3975)
POST /zot/auth/logout now returns an endSessionUrl in the JSON response body when the session was established via an OIDC provider whose discovery document advertises an endSessionEndpoint, so the UI can navigate the browser to it and terminate the session at the IdP in addition to clearing the local cookie. - The OIDC callback records the provider name in the session after login; the github OAuth2 path is untouched. - end_session_endpoint is read from the zitadel/oidc RelyingParty and validated as an absolute http(s) URL. - post_logout_redirect_uri prefers http.externalUrl when set and falls back to deriving the origin from the incoming request. - No id_token_hint is sent; client_id identifies the RP, so the ID token does not need to be persisted. - Non-OIDC sessions (local/basic/LDAP/GitHub) retain the existing 200 OK, no body behavior. Operators must register the URI zot sends as a valid post-logout redirect URI on the IdP client. Ref: https://openid.net/specs/openid-connect-rpinitiated-1_0.html Signed-off-by: Nikita Vakula <programmistov.programmist@gmail.com>
1 parent 8bec9b3 commit 8282aef

8 files changed

Lines changed: 703 additions & 38 deletions

File tree

errors/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,5 @@ var (
208208
ErrOIDCAudienceMismatch = errors.New("token audience does not match any of the expected audiences")
209209
ErrCertificateNotLoaded = errors.New("tls certificate not yet loaded")
210210
ErrCertificateWatcherAlreadyRunning = errors.New("certificate watcher is already running")
211+
ErrInvalidEndSessionEndpoint = errors.New("end_session_endpoint must be an absolute http(s) URL")
211212
)

pkg/api/authn.go

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, userAc *reqCtx.UserAcce
204204
// saved logged session only if the request comes from web (has UI session header value)
205205
if hasSessionHeader(request) {
206206
secure := ctlr.Config.UseSecureSession()
207-
if err := saveUserLoggedSession(cookieStore, response, request, identity, secure, ctlr.Log); err != nil {
207+
if err := saveUserLoggedSession(cookieStore, response, request, identity, "", secure, ctlr.Log); err != nil {
208208
return false, err
209209
}
210210
}
@@ -243,7 +243,7 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, userAc *reqCtx.UserAcce
243243
// saved logged session only if the request comes from web (has UI session header value)
244244
if hasSessionHeader(request) {
245245
secure := ctlr.Config.UseSecureSession()
246-
if err := saveUserLoggedSession(cookieStore, response, request, identity, secure, ctlr.Log); err != nil {
246+
if err := saveUserLoggedSession(cookieStore, response, request, identity, "", secure, ctlr.Log); err != nil {
247247
return false, err
248248
}
249249
}
@@ -901,6 +901,24 @@ func NewRelyingPartyGithub(config *config.Config, provider string, hashKey, encr
901901
return relyingParty
902902
}
903903

904+
// originFromConfig returns the server's base URL (scheme + host[:port], no trailing
905+
// slash) used as the origin for OIDC redirect URIs. It prefers ExternalURL; otherwise
906+
// it derives the origin from cfg.HTTP.Address, cfg.HTTP.Port, and cfg.HTTP.TLS. Using
907+
// a single source for both the login redirect_uri and the logout
908+
// post_logout_redirect_uri ensures the IdP sees matching origins.
909+
func originFromConfig(cfg *config.Config) string {
910+
if trimmed := strings.TrimSuffix(cfg.HTTP.ExternalURL, "/"); trimmed != "" {
911+
return trimmed
912+
}
913+
914+
scheme := constants.SchemeHTTP
915+
if cfg.HTTP.TLS != nil {
916+
scheme = constants.SchemeHTTPS
917+
}
918+
919+
return scheme + "://" + net.JoinHostPort(cfg.HTTP.Address, cfg.HTTP.Port)
920+
}
921+
904922
func getRelyingPartyArgs(cfg *config.Config, provider string, hashKey, encryptKey []byte, log log.Logger) (
905923
string, string, string, string, []string, []rp.Option,
906924
) {
@@ -918,26 +936,11 @@ func getRelyingPartyArgs(cfg *config.Config, provider string, hashKey, encryptKe
918936
scopes = append([]string{oidc.ScopeOpenID}, scopes...)
919937
}
920938

921-
port := cfg.HTTP.Port
922939
issuer := providerConfig.Issuer
923940
keyPath := providerConfig.KeyPath
924-
baseURL := net.JoinHostPort(cfg.HTTP.Address, port)
925941

926942
callback := constants.CallbackBasePath + "/" + provider
927-
928-
var redirectURI string
929-
930-
if cfg.HTTP.ExternalURL != "" {
931-
externalURL := strings.TrimSuffix(cfg.HTTP.ExternalURL, "/")
932-
redirectURI = fmt.Sprintf("%s%s", externalURL, callback)
933-
} else {
934-
scheme := constants.SchemeHTTP
935-
if cfg.HTTP.TLS != nil {
936-
scheme = constants.SchemeHTTPS
937-
}
938-
939-
redirectURI = fmt.Sprintf("%s://%s%s", scheme, baseURL, callback)
940-
}
943+
redirectURI := originFromConfig(cfg) + callback
941944

942945
options := []rp.Option{
943946
rp.WithVerifierOpts(rp.WithIssuedAtOffset(issuedAtOffset)),
@@ -1066,7 +1069,7 @@ func GetGithubUserInfo(ctx context.Context, client *github.Client, log log.Logge
10661069
}
10671070

10681071
func saveUserLoggedSession(cookieStore sessions.Store, response http.ResponseWriter,
1069-
request *http.Request, identity string, secure bool, log log.Logger,
1072+
request *http.Request, identity, provider string, secure bool, log log.Logger,
10701073
) error {
10711074
session, _ := cookieStore.Get(request, "session")
10721075

@@ -1076,6 +1079,12 @@ func saveUserLoggedSession(cookieStore sessions.Store, response http.ResponseWri
10761079
session.Values["authStatus"] = true
10771080
session.Values["user"] = identity
10781081

1082+
if provider != "" {
1083+
session.Values["provider"] = provider
1084+
} else {
1085+
delete(session.Values, "provider")
1086+
}
1087+
10791088
// let the session set its own id
10801089
err := session.Save(request, response)
10811090
if err != nil {
@@ -1098,7 +1107,7 @@ func saveUserLoggedSession(cookieStore sessions.Store, response http.ResponseWri
10981107
}
10991108

11001109
// OAuth2Callback is the callback logic where openid/oauth2 will redirect back to our app.
1101-
func OAuth2Callback(ctlr *Controller, w http.ResponseWriter, r *http.Request, state, email string,
1110+
func OAuth2Callback(ctlr *Controller, w http.ResponseWriter, r *http.Request, state, email, provider string,
11021111
groups []string,
11031112
) (string, error) {
11041113
stateCookie, _ := ctlr.CookieStore.Get(r, "statecookie")
@@ -1126,7 +1135,7 @@ func OAuth2Callback(ctlr *Controller, w http.ResponseWriter, r *http.Request, st
11261135
// if this line has been reached, then a new session should be created
11271136
// if the `session` key is already on the cookie, it's not a valid one
11281137
secure := ctlr.Config.UseSecureSession()
1129-
if err := saveUserLoggedSession(ctlr.CookieStore, w, r, email, secure, ctlr.Log); err != nil {
1138+
if err := saveUserLoggedSession(ctlr.CookieStore, w, r, email, provider, secure, ctlr.Log); err != nil {
11301139
return "", err
11311140
}
11321141

0 commit comments

Comments
 (0)