Sanitize OIDC discovery errors and require CSRF on HEAD and OPTIONS
Independent review Codex gpt-6-sol high: APPROVED (0 findings)
CI / ci (pull_request) Successful in 1m26s
CI / ci (push) Successful in 1m26s

F03-01: go-oidc embeds the provider's response body in its error for a
non-200 discovery response, which cmd/server logged at startup. Discovery
now runs through a status-recording transport and a non-200 response is
reported as "provider answered HTTP <status>" only; transport errors keep
their cause. A test with a sentinel body proves it reaches neither the
error nor the log.

F03-02: the CSRF middleware exempts only GET, so HEAD and OPTIONS under
/api/v1 without X-MealQuest-CSRF get 403 csrf, matching CONTEXT.MD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 11:11:29 -04:00
co-authored by Claude Opus 5.5
parent 5833a73541
commit e73ebe2f2f
5 changed files with 76 additions and 10 deletions
+32 -2
View File
@@ -13,6 +13,7 @@ import (
"net/http"
"net/url"
"slices"
"sync/atomic"
"time"
"github.com/coreos/go-oidc/v3/oidc"
@@ -107,9 +108,12 @@ func New(ctx context.Context, cfg Config, sessions Sessions, logger *slog.Logger
}
client := &http.Client{Timeout: providerTimeout, CheckRedirect: checkRedirect}
provider, err := oidc.NewProvider(oidc.ClientContext(ctx, client), cfg.Issuer)
discovery := *client
recorder := &statusRecorder{next: http.DefaultTransport}
discovery.Transport = recorder
provider, err := oidc.NewProvider(oidc.ClientContext(ctx, &discovery), cfg.Issuer)
if err != nil {
return nil, fmt.Errorf("auth: OIDC discovery: %w", err)
return nil, discoveryError(err, int(recorder.status.Load()))
}
endpoint := provider.Endpoint()
var discovered struct {
@@ -159,6 +163,32 @@ func New(ctx context.Context, cfg Config, sessions Sessions, logger *slog.Logger
}, nil
}
// statusRecorder records the status of the last response it carries.
type statusRecorder struct {
next http.RoundTripper
status atomic.Int32
}
func (s *statusRecorder) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := s.next.RoundTrip(req)
if err == nil {
s.status.Store(int32(resp.StatusCode))
}
return resp, err
}
// discoveryError reports a failed discovery. go-oidc puts the provider's
// response body in its error for a non-200 discovery response, so that
// error is replaced by the status alone. Transport errors carry no response
// content and keep their cause.
func discoveryError(err error, status int) error {
var urlErr *url.Error
if !errors.As(err, &urlErr) && status != 0 && status != http.StatusOK {
return fmt.Errorf("auth: OIDC discovery: provider answered HTTP %d", status)
}
return fmt.Errorf("auth: OIDC discovery: %w", err)
}
// checkEndpoint applies secureurl.Check to the provider URL raw, naming the
// endpoint in the error.
func checkEndpoint(name, raw string) error {