Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 24 additions & 20 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,24 @@ import (
)

var (
serverURL string
clientID string
tokenFile string
tokenStoreMode string
flagServerURL *string
flagClientID *string
flagTokenFile *string
flagTokenStore *string
configInitialized bool
retryClient *retry.Client
tokenStore credstore.Store[credstore.Token]
serverURL string
clientID string
tokenFile string
tokenStoreMode string
flagServerURL *string
flagClientID *string
flagTokenFile *string
flagTokenStore *string
configOnce sync.Once
retryClient *retry.Client
tokenStore credstore.Store[credstore.Token]
)

const defaultKeyringService = "authgate-device-cli"

// maxResponseBodySize limits HTTP response body reads to prevent memory exhaustion (DoS).
const maxResponseBodySize = 1 << 20 // 1 MB
Comment on lines +46 to +47
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces a new security boundary (maxResponseBodySize) but there are no tests exercising the oversized-response path (e.g., server returns >1MB, client should fail in a well-defined way). Since this file already has extensive HTTP/flow tests, consider adding a test case that returns a payload just over the limit and asserts the resulting error (ideally a dedicated "response too large" error if you add explicit truncation detection).

Copilot uses AI. Check for mistakes.

// Timeout configuration for different operations
const (
deviceCodeRequestTimeout = 10 * time.Second
Expand Down Expand Up @@ -107,11 +110,12 @@ func init() {
// initConfig parses flags and initializes configuration
// Separated from init() to avoid conflicts with test flag parsing
func initConfig() {
if configInitialized {
return
}
configInitialized = true
configOnce.Do(func() {
doInitConfig()
})
}

func doInitConfig() {
flag.Parse()

// Priority: flag > env > default
Expand Down Expand Up @@ -438,7 +442,7 @@ func requestDeviceCode(ctx context.Context) (*oauth2.DeviceAuthResponse, error)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
Expand Down Expand Up @@ -638,7 +642,7 @@ func exchangeDeviceCode(
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
Comment on lines +645 to 648
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same pattern as elsewhere: io.LimitReader truncates silently at maxResponseBodySize, so an oversized response will be treated as valid input for later parsing/handling and may surface as misleading unexpected EOF/JSON errors. Consider reading maxResponseBodySize+1 and returning an explicit "response too large" error when exceeded.

Suggested change
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// Read up to maxResponseBodySize+1 bytes so we can detect oversized responses
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize+1))
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if len(body) > int(maxResponseBodySize) {
return nil, fmt.Errorf("response too large")
}

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -696,7 +700,7 @@ func verifyToken(ctx context.Context, accessToken string, d tui.Displayer) error
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
Comment on lines +703 to 706
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

io.ReadAll(io.LimitReader(...)) will not tell you whether the server sent more than maxResponseBodySize; it just stops at the limit. Consider detecting oversize bodies (read max+1, check length) so callers get a clear "response too large" error rather than parse/format errors from truncated content.

Suggested change
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
limitedReader := io.LimitReader(resp.Body, int64(maxResponseBodySize)+1)
body, err := io.ReadAll(limitedReader)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
if int64(len(body)) > int64(maxResponseBodySize) {
return fmt.Errorf("failed to read response: response body too large")
}

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -746,7 +750,7 @@ func refreshAccessToken(
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return credstore.Token{}, fmt.Errorf("failed to read response: %w", err)
}
Comment on lines +753 to 756
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because io.LimitReader truncates without error, a response slightly over maxResponseBodySize will be truncated and then unmarshaled/printed, producing confusing errors and potentially incomplete diagnostics. Consider explicitly checking for truncation (read max+1 and error if exceeded).

Suggested change
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
return credstore.Token{}, fmt.Errorf("failed to read response: %w", err)
}
// Read up to maxResponseBodySize+1 bytes so we can detect truncation explicitly.
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize+1))
if err != nil {
return credstore.Token{}, fmt.Errorf("failed to read response: %w", err)
}
if len(body) > maxResponseBodySize {
return credstore.Token{}, fmt.Errorf("failed to read response: body exceeds maximum allowed size")
}

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -871,7 +875,7 @@ func makeAPICallWithAutoRefresh(
defer resp.Body.Close()
}

body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize))
if err != nil {
Comment on lines 875 to 879
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the 401 retry path, the first resp.Body is only closed via the earlier deferred close, so it stays open while refreshAccessToken and the retry request run. That can temporarily hold onto connections/resources unnecessarily. Consider explicitly closing (and optionally draining) the original resp.Body immediately before starting the refresh/retry, and only deferring the close for the response you actually read from.

Copilot uses AI. Check for mistakes.
return fmt.Errorf("failed to read response: %w", err)
}
Comment on lines +878 to 881
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reading the API response body, io.LimitReader prevents large allocations but doesn't indicate truncation. If the upstream returns >maxResponseBodySize, you'll operate on a truncated body. Consider detecting and returning a specific "response too large" error (e.g., read max+1 and check length).

Copilot uses AI. Check for mistakes.
Expand Down
Loading