From 02deb758961fe2a2d609749675f1b63e08ce1cb8 Mon Sep 17 00:00:00 2001 From: aniket866 Date: Fri, 16 Oct 2026 01:19:41 +0530 Subject: [PATCH 1/2] fix: isolate the SSO callback server Signed-off-by: aniket866 --- cmd/login.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index 5953ed6..6a7b3ab 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -24,6 +24,7 @@ import ( "fmt" "html" "log" + "net" "net/http" "os" "strconv" @@ -228,7 +229,7 @@ func oauth2login( // handledRequests ensures we do not handle more requests than necessary handledRequests := 0 // completionChan is to signal flow completed. Non-empty string indicates error - completionChan := make(chan string) + completionChan := make(chan string, 1) stateNonce, err := rand.String(24) if err != nil { @@ -305,11 +306,13 @@ func oauth2login( } // G112: Set ReadHeaderTimeout to mitigate Slowloris DoS attack. + mux := http.NewServeMux() + mux.HandleFunc("/auth/callback", callbackHandler) srv := &http.Server{ Addr: "localhost:" + strconv.Itoa(port), + Handler: mux, ReadHeaderTimeout: 3 * time.Second, } - http.HandleFunc("/auth/callback", callbackHandler) var url string opts := []oauth2.AuthCodeOption{} @@ -322,15 +325,20 @@ func oauth2login( authBaseURL := strings.SplitN(url, "?", 2)[0] fmt.Printf("Performing %s flow login: %s\n", "authorization_code", authBaseURL) time.Sleep(1 * time.Second) - if err := ssoAuthFlow(url, ssoLaunchBrowser); err != nil { - return "", "", err + listener, err := net.Listen("tcp", srv.Addr) + if err != nil { + return "", "", errors.Wrap(errors.KindEnvironment, fmt.Errorf("starting temporary HTTP server on %s: %w", srv.Addr, err)) } + defer srv.Close() go func() { log.Printf("Listen: %s\n", srv.Addr) - if err := srv.ListenAndServe(); err != http.ErrServerClosed { + if err := srv.Serve(listener); err != http.ErrServerClosed { completionChan <- fmt.Sprintf("temporary HTTP server failed: %s", err) } }() + if err := ssoAuthFlow(url, ssoLaunchBrowser); err != nil { + return "", "", err + } errMsg := <-completionChan if errMsg != "" { return "", "", errors.Wrapf(errors.KindGeneric, "%s", errMsg) From c7e5300e391c672bab719e1b6dc15929fd507fcf Mon Sep 17 00:00:00 2001 From: aniket866 Date: Fri, 16 Oct 2026 01:31:30 +0530 Subject: [PATCH 2/2] test: cover isolated SSO callback servers Signed-off-by: aniket866 --- cmd/login.go | 28 ++++++++----- cmd/login_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 cmd/login_test.go diff --git a/cmd/login.go b/cmd/login.go index 6a7b3ab..a68a437 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -305,15 +305,6 @@ func oauth2login( completionChan <- "" } - // G112: Set ReadHeaderTimeout to mitigate Slowloris DoS attack. - mux := http.NewServeMux() - mux.HandleFunc("/auth/callback", callbackHandler) - srv := &http.Server{ - Addr: "localhost:" + strconv.Itoa(port), - Handler: mux, - ReadHeaderTimeout: 3 * time.Second, - } - var url string opts := []oauth2.AuthCodeOption{} opts = append(opts, oauth2.SetAuthURLParam("client_id", "microcks-app-js")) @@ -325,9 +316,9 @@ func oauth2login( authBaseURL := strings.SplitN(url, "?", 2)[0] fmt.Printf("Performing %s flow login: %s\n", "authorization_code", authBaseURL) time.Sleep(1 * time.Second) - listener, err := net.Listen("tcp", srv.Addr) + srv, listener, err := newOAuthCallbackServer("localhost:"+strconv.Itoa(port), callbackHandler) if err != nil { - return "", "", errors.Wrap(errors.KindEnvironment, fmt.Errorf("starting temporary HTTP server on %s: %w", srv.Addr, err)) + return "", "", err } defer srv.Close() go func() { @@ -353,6 +344,21 @@ func oauth2login( return tokenString, refreshToken, nil } +func newOAuthCallbackServer(addr string, callbackHandler http.HandlerFunc) (*http.Server, net.Listener, error) { + mux := http.NewServeMux() + mux.HandleFunc("/auth/callback", callbackHandler) + srv := &http.Server{ + Addr: addr, + Handler: mux, + ReadHeaderTimeout: 3 * time.Second, + } + listener, err := net.Listen("tcp", srv.Addr) + if err != nil { + return nil, nil, errors.Wrap(errors.KindEnvironment, fmt.Errorf("starting temporary HTTP server on %s: %w", srv.Addr, err)) + } + return srv, listener, nil +} + func ssoAuthFlow(url string, ssoLaunchBrowser bool) error { if ssoLaunchBrowser { fmt.Printf("Opening system default browser for authentication\n") diff --git a/cmd/login_test.go b/cmd/login_test.go new file mode 100644 index 0000000..c49c2db --- /dev/null +++ b/cmd/login_test.go @@ -0,0 +1,103 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + stderrors "errors" + "net" + "net/http" + "testing" + + clierrors "github.com/microcks/microcks-cli/pkg/errors" +) + +func TestNewOAuthCallbackServerUsesIsolatedMuxes(t *testing.T) { + firstStatus := startOAuthCallbackTestServer(t, http.StatusCreated) + secondStatus := startOAuthCallbackTestServer(t, http.StatusAccepted) + + if firstStatus != http.StatusCreated { + t.Fatalf("first callback server returned status %d, want %d", firstStatus, http.StatusCreated) + } + if secondStatus != http.StatusAccepted { + t.Fatalf("second callback server returned status %d, want %d", secondStatus, http.StatusAccepted) + } +} + +func TestNewOAuthCallbackServerReturnsErrorWhenPortIsOccupied(t *testing.T) { + occupiedListener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := occupiedListener.Close(); err != nil { + t.Errorf("closing occupied listener: %v", err) + } + }) + + server, listener, err := newOAuthCallbackServer(occupiedListener.Addr().String(), http.NotFound) + if err == nil { + if server != nil { + if closeErr := server.Close(); closeErr != nil { + t.Errorf("closing unexpected callback server: %v", closeErr) + } + } + if listener != nil { + if closeErr := listener.Close(); closeErr != nil { + t.Errorf("closing unexpected callback listener: %v", closeErr) + } + } + t.Fatal("expected an error when the callback port is already occupied") + } + if got := clierrors.KindOf(err); got != clierrors.KindEnvironment { + t.Fatalf("error kind = %v, want %v", got, clierrors.KindEnvironment) + } +} + +func startOAuthCallbackTestServer(t *testing.T, status int) int { + t.Helper() + + server, listener, err := newOAuthCallbackServer("127.0.0.1:0", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(status) + }) + if err != nil { + t.Fatal(err) + } + + serveErrors := make(chan error, 1) + go func() { + serveErrors <- server.Serve(listener) + }() + t.Cleanup(func() { + if err := server.Close(); err != nil { + t.Errorf("closing callback server: %v", err) + } + if err := <-serveErrors; !stderrors.Is(err, http.ErrServerClosed) { + t.Errorf("callback server returned unexpected error: %v", err) + } + }) + + response, err := http.Get("http://" + listener.Addr().String() + "/auth/callback") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := response.Body.Close(); err != nil { + t.Errorf("closing callback response body: %v", err) + } + }() + return response.StatusCode +}