From d308201803b16c86ada3204af0a16b1ec840255e Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Sat, 25 Jul 2026 23:37:22 +0200 Subject: [PATCH] fix(api): logout's post_logout_redirect_uri uses FrontendURL, not BackendURL Same class of bug as the OIDC callback fix: handleSessionLogout redirected Keycloak's end-session flow back to BackendURL+"/", which 404s in a split-origin deployment (the backend serves no "/" route). Flagged as a known-deferred question in the callback-redirect design spec; fixing it now that it's been hit in practice. Co-Authored-By: Claude Sonnet 5 --- backend/internal/api/api_test.go | 10 ++++++++++ backend/internal/api/session.go | 20 ++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/backend/internal/api/api_test.go b/backend/internal/api/api_test.go index 5c149f4..cabf6ca 100644 --- a/backend/internal/api/api_test.go +++ b/backend/internal/api/api_test.go @@ -923,3 +923,13 @@ func TestSessionLogout_ClearsSessionCookieAndRedirectsToEndSession(t *testing.T) t.Fatalf("expected session cookie to be cleared, got %+v", cleared) } } + +func TestSessionLogout_PostLogoutRedirectUsesFrontendURL(t *testing.T) { + verifier := &authmock.Verifier{} // EndSessionResult unset: echoes back postLogoutRedirectURL unchanged + s, _ := newTestServerWithAuth(t, verifier) + rec := doJSON(t, s.Router(), http.MethodPost, "/api/session/logout", nil) + + if rec.Code != http.StatusFound || rec.Header().Get("Location") != testSessionConfig.FrontendURL+"/" { + t.Fatalf("status = %d, Location = %q, want post_logout_redirect_uri = %q", rec.Code, rec.Header().Get("Location"), testSessionConfig.FrontendURL+"/") + } +} diff --git a/backend/internal/api/session.go b/backend/internal/api/session.go index 46d5a5f..9ec3e07 100644 --- a/backend/internal/api/session.go +++ b/backend/internal/api/session.go @@ -16,14 +16,18 @@ type SessionConfig struct { Duration time.Duration Secure bool // BackendURL is this app's own externally reachable origin (e.g. - // "https://geniusrun.example.com", no trailing slash), used to build an - // absolute post_logout_redirect_uri for the identity provider -- some - // providers, including Keycloak, require this to be an absolute URL - // matching one registered on the client, not a bare relative path. + // "https://geniusrun.example.com", no trailing slash) -- derives + // OIDCRedirectURL (config.Config), the only thing that must stay pointed + // at the backend itself, since that's where /api/session/callback is + // actually served. BackendURL string - // FrontendURL is the origin the browser should land on after the OIDC - // callback (success or failure) -- see config.Config.FrontendURL for why - // this can differ from BackendURL in a split-origin deployment. + // FrontendURL is the origin the browser should land on after any + // user-facing redirect: the OIDC callback (success or failure) and the + // post_logout_redirect_uri sent to the identity provider on logout. Some + // providers, including Keycloak, require an absolute URL matching one + // registered on the client, not a bare relative path -- see + // config.Config.FrontendURL for why this can differ from BackendURL in a + // split-origin deployment. FrontendURL string } @@ -87,7 +91,7 @@ func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) { func (s *Server) handleSessionLogout(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, auth.ClearCookie(auth.SessionCookieName, s.Session.Secure)) - http.Redirect(w, r, s.Auth.EndSessionURL(s.Session.BackendURL+"/"), http.StatusFound) + http.Redirect(w, r, s.Auth.EndSessionURL(s.Session.FrontendURL+"/"), http.StatusFound) } func (s *Server) handleSessionMe(w http.ResponseWriter, r *http.Request) {