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 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 23:37:22 +02:00
parent 099e746119
commit d308201803
2 changed files with 22 additions and 8 deletions

View File

@@ -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+"/")
}
}

View File

@@ -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) {