diff --git a/backend/cmd/geniusrund/main.go b/backend/cmd/geniusrund/main.go index aec7434..e5103ba 100644 --- a/backend/cmd/geniusrund/main.go +++ b/backend/cmd/geniusrund/main.go @@ -52,6 +52,7 @@ func main() { Duration: cfg.SessionDuration, Secure: cfg.SessionSecure, PublicBaseURL: cfg.PublicBaseURL, + FrontendURL: cfg.FrontendURL, }) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) diff --git a/backend/internal/api/api_test.go b/backend/internal/api/api_test.go index a5cbd8b..380feb4 100644 --- a/backend/internal/api/api_test.go +++ b/backend/internal/api/api_test.go @@ -28,6 +28,7 @@ var testSessionConfig = SessionConfig{ Duration: time.Hour, Secure: false, PublicBaseURL: "https://geniusrun.example.com", + FrontendURL: "https://app.geniusrun.example.com", } func newTestServer(t *testing.T) (*Server, *store.DB, int64) { @@ -828,7 +829,7 @@ func TestSessionCallback_AuthorizedSetsSessionCookieAndRedirectsHome(t *testing. rec := httptest.NewRecorder() s.Router().ServeHTTP(rec, req) - if rec.Code != http.StatusFound || rec.Header().Get("Location") != "/" { + if rec.Code != http.StatusFound || rec.Header().Get("Location") != "https://app.geniusrun.example.com/" { t.Fatalf("status = %d, Location = %q", rec.Code, rec.Header().Get("Location")) } var sessionCookie *http.Cookie @@ -864,7 +865,7 @@ func TestSessionCallback_UnauthorizedRedirectsWithoutSessionCookie(t *testing.T) rec := httptest.NewRecorder() s.Router().ServeHTTP(rec, req) - if rec.Code != http.StatusFound || rec.Header().Get("Location") != "/?auth_error=forbidden" { + if rec.Code != http.StatusFound || rec.Header().Get("Location") != "https://app.geniusrun.example.com/?auth_error=forbidden" { t.Fatalf("status = %d, Location = %q", rec.Code, rec.Header().Get("Location")) } for _, c := range rec.Result().Cookies() { @@ -879,7 +880,7 @@ func TestSessionCallback_MissingTxnCookieRedirectsFailed(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/session/callback?code=abc&state=s1", nil) rec := httptest.NewRecorder() s.Router().ServeHTTP(rec, req) - if rec.Code != http.StatusFound || rec.Header().Get("Location") != "/?auth_error=failed" { + if rec.Code != http.StatusFound || rec.Header().Get("Location") != "https://app.geniusrun.example.com/?auth_error=failed" { t.Fatalf("status = %d, Location = %q", rec.Code, rec.Header().Get("Location")) } } diff --git a/backend/internal/api/session.go b/backend/internal/api/session.go index 50e3e53..5519a8e 100644 --- a/backend/internal/api/session.go +++ b/backend/internal/api/session.go @@ -21,6 +21,10 @@ type SessionConfig struct { // providers, including Keycloak, require this to be an absolute URL // matching one registered on the client, not a bare relative path. PublicBaseURL 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 PublicBaseURL in a split-origin deployment. + FrontendURL string } type sessionMeResponse struct { @@ -49,7 +53,7 @@ func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) { txnCookie, err := r.Cookie(auth.TxnCookieName) if err != nil { log.Printf("session callback: missing txn cookie: %v", err) - http.Redirect(w, r, "/?auth_error=failed", http.StatusFound) + http.Redirect(w, r, s.Session.FrontendURL+"/?auth_error=failed", http.StatusFound) return } http.SetCookie(w, auth.ClearCookie(auth.TxnCookieName, s.Session.Secure)) @@ -57,18 +61,18 @@ func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) { txn, err := auth.ParseTxnCookie(txnCookie, s.Session.Secret) if err != nil { log.Printf("session callback: failed to parse txn cookie: %v", err) - http.Redirect(w, r, "/?auth_error=failed", http.StatusFound) + http.Redirect(w, r, s.Session.FrontendURL+"/?auth_error=failed", http.StatusFound) return } result, err := s.Auth.HandleCallback(r.Context(), txn, r.URL.Query()) if err != nil { log.Printf("session callback: HandleCallback failed (state mismatch, code exchange, or ID-token verification): %v", err) - http.Redirect(w, r, "/?auth_error=failed", http.StatusFound) + http.Redirect(w, r, s.Session.FrontendURL+"/?auth_error=failed", http.StatusFound) return } if !result.Authorized { - http.Redirect(w, r, "/?auth_error=forbidden", http.StatusFound) + http.Redirect(w, r, s.Session.FrontendURL+"/?auth_error=forbidden", http.StatusFound) return } @@ -78,7 +82,7 @@ func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) { return } http.SetCookie(w, sessionCookie) - http.Redirect(w, r, "/", http.StatusFound) + http.Redirect(w, r, s.Session.FrontendURL+"/", http.StatusFound) } func (s *Server) handleSessionLogout(w http.ResponseWriter, r *http.Request) {