From 9e7a50b443e564555efc02c56eb5c12c0f95047c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:58:27 +0000 Subject: [PATCH] feat: add WWWAuthenticate field + Challenge(w) method to RequestAuthenticator Co-authored-by: coolaj86 <122831+coolaj86@users.noreply.github.com> --- auth/request.go | 17 ++++++++++++++++- cmd/auth-proxy/main.go | 22 +++++++++++++--------- cmd/smsapid/main.go | 5 +++-- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/auth/request.go b/auth/request.go index 883af62..8036c93 100644 --- a/auth/request.go +++ b/auth/request.go @@ -36,17 +36,32 @@ type RequestAuthenticator struct { // TokenQueryParams lists query parameter names checked for tokens, // e.g. []string{"access_token", "token"}. TokenQueryParams []string + + // WWWAuthenticate is the value sent in the WWW-Authenticate response header + // when Challenge is called. An empty string disables the header. + // NewRequestAuthenticator sets this to "Basic". + WWWAuthenticate string } // NewRequestAuthenticator returns a RequestAuthenticator with sane defaults: // Basic Auth enabled, Bearer/Token Authorization schemes, common API-key -// headers, and access_token/token query params. +// headers, access_token/token query params, and WWW-Authenticate: Basic. func NewRequestAuthenticator() *RequestAuthenticator { return &RequestAuthenticator{ AuthenticateBasic: true, AuthorizationSchemes: []string{"Bearer", "Token"}, TokenHeaders: []string{"X-API-Key", "X-Auth-Token", "X-Access-Token"}, TokenQueryParams: []string{"access_token", "token"}, + WWWAuthenticate: "Basic", + } +} + +// Challenge sets the WWW-Authenticate response header to ra.WWWAuthenticate +// when it is non-empty. Call this before writing a 401 Unauthorized response +// so that clients know which auth scheme to use. +func (ra *RequestAuthenticator) Challenge(w http.ResponseWriter) { + if ra.WWWAuthenticate != "" { + w.Header().Set("WWW-Authenticate", ra.WWWAuthenticate) } } diff --git a/cmd/auth-proxy/main.go b/cmd/auth-proxy/main.go index 4d1567a..20b96ce 100644 --- a/cmd/auth-proxy/main.go +++ b/cmd/auth-proxy/main.go @@ -76,6 +76,7 @@ type MainConfig struct { ProxyTarget string AES128KeyPath string ShowVersion bool + WWWAuthenticate string AuthorizationHeaderSchemes []string TokenHeaderNames []string QueryParamNames []string @@ -84,6 +85,7 @@ type MainConfig struct { tokenSchemeList string tokenHeaderList string tokenParamList string + ra *auth.RequestAuthenticator } func (c *MainConfig) Addr() string { @@ -102,6 +104,7 @@ func main() { tokenSchemeList: "", tokenHeaderList: "", tokenParamList: "", + WWWAuthenticate: "Basic", AuthorizationHeaderSchemes: nil, // []string{"Bearer", "Token"} TokenHeaderNames: nil, // []string{"X-API-Key", "X-Auth-Token", "X-Access-Token"}, QueryParamNames: nil, // []string{"access_token", "token"}, @@ -282,6 +285,14 @@ func run(cli *MainConfig) { log.Fatalf("Failed to load CSV auth: %v", err) } + cli.ra = &auth.RequestAuthenticator{ + Authenticator: creds, + AuthorizationSchemes: cli.AuthorizationHeaderSchemes, + TokenHeaders: cli.TokenHeaderNames, + TokenQueryParams: cli.QueryParamNames, + WWWAuthenticate: cli.WWWAuthenticate, + } + var usableRoles int for key := range creds.CredentialKeys() { u, err := creds.LoadCredential(key) @@ -391,8 +402,7 @@ func (cli *MainConfig) newAuthProxyHandler(targetURL string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !cli.authorize(r) { - // TODO allow --realm for `WWW-Authenticate: Basic realm="My Application"` - w.Header().Set("WWW-Authenticate", `Basic`) + cli.ra.Challenge(w) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } @@ -593,13 +603,7 @@ func matchPattern(grant, rMethod, rHost, rPath string) bool { } func (cli *MainConfig) authenticate(r *http.Request) (auth.BasicPrinciple, error) { - ra := auth.RequestAuthenticator{ - Authenticator: creds, - AuthorizationSchemes: cli.AuthorizationHeaderSchemes, - TokenHeaders: cli.TokenHeaderNames, - TokenQueryParams: cli.QueryParamNames, - } - cred, err := ra.Authenticate(r) + cred, err := cli.ra.Authenticate(r) if errors.Is(err, auth.ErrNoCredentials) { return nil, ErrNoAuth } diff --git a/cmd/smsapid/main.go b/cmd/smsapid/main.go index 9c36505..e123a5e 100644 --- a/cmd/smsapid/main.go +++ b/cmd/smsapid/main.go @@ -39,6 +39,7 @@ var smsRequestAuth = &auth.RequestAuthenticator{ AuthenticateBasic: true, AuthorizationSchemes: []string{"*"}, TokenHeaders: []string{"API-Key", "X-API-Key"}, + WWWAuthenticate: "Basic", } func main() { @@ -145,13 +146,13 @@ func requireSMSPermission(permission string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if smsAuth == nil { - w.Header().Set("WWW-Authenticate", `Basic`) + smsRequestAuth.Challenge(w) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } cred, err := smsRequestAuth.Authenticate(r) if err != nil || !hasSMSPermission(cred.Permissions(), permission) { - w.Header().Set("WWW-Authenticate", `Basic`) + smsRequestAuth.Challenge(w) http.Error(w, "Unauthorized", http.StatusUnauthorized) return }