From c8eabab03d4e7ba91ee30e9435474a6cff978032 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:17:50 +0000 Subject: [PATCH] refactor: extract RequestAuthenticator into auth package; use in smsapid and auth-proxy Co-authored-by: coolaj86 <122831+coolaj86@users.noreply.github.com> --- auth/request.go | 77 ++++++++++++++++++++++++++++++++++++++++++ cmd/auth-proxy/go.mod | 9 +++-- cmd/auth-proxy/main.go | 50 +++++++-------------------- cmd/smsapid/go.mod | 1 + cmd/smsapid/main.go | 32 +++--------------- 5 files changed, 101 insertions(+), 68 deletions(-) create mode 100644 auth/request.go diff --git a/auth/request.go b/auth/request.go new file mode 100644 index 0000000..05c3c44 --- /dev/null +++ b/auth/request.go @@ -0,0 +1,77 @@ +package auth + +import ( + "errors" + "net/http" + "slices" + "strings" +) + +// ErrNoCredentials is returned by RequestAuthenticator.Authenticate when the +// request contains no recognizable form of credentials. +var ErrNoCredentials = errors.New("no credentials provided") + +// RequestAuthenticator extracts credentials from an HTTP request and delegates +// verification to a BasicAuthenticator. It supports Basic Auth, Authorization +// header tokens, custom token headers, and query-parameter tokens. +type RequestAuthenticator struct { + // AuthorizationSchemes lists accepted schemes for "Authorization: ". + // nil accepts any scheme; a non-nil empty slice skips the Authorization header + // entirely; ["*"] also accepts any scheme; ["Bearer", "Token"] restricts to those. + AuthorizationSchemes []string + + // TokenHeaders lists header names checked for bearer tokens, + // e.g. []string{"API-Key", "X-API-Key"}. + TokenHeaders []string + + // TokenQueryParams lists query parameter names checked for tokens, + // e.g. []string{"access_token", "token"}. + TokenQueryParams []string +} + +// Authenticate extracts credentials from r in this order: +// 1. Basic Auth (Authorization: Basic …) +// 2. Authorization: (filtered by AuthorizationSchemes) +// 3. Token headers (TokenHeaders) +// 4. Query parameters (TokenQueryParams) +// +// Returns ErrNoCredentials if no credential form is present in the request. +func (ra *RequestAuthenticator) Authenticate(r *http.Request, a BasicAuthenticator) (BasicPrinciple, error) { + // 1. Basic Auth + if username, password, ok := r.BasicAuth(); ok { + return a.Authenticate(username, password) + } + + // 2. Authorization: + // nil AuthorizationSchemes accepts any scheme; a non-nil empty slice skips. + if ra.AuthorizationSchemes == nil || len(ra.AuthorizationSchemes) > 0 { + if authHeader := r.Header.Get("Authorization"); authHeader != "" { + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) == 2 { + scheme, token := parts[0], strings.TrimSpace(parts[1]) + if ra.AuthorizationSchemes == nil || + ra.AuthorizationSchemes[0] == "*" || + slices.Contains(ra.AuthorizationSchemes, scheme) { + return a.Authenticate("", token) + } + } + return nil, ErrNoCredentials + } + } + + // 3. Token headers + for _, h := range ra.TokenHeaders { + if token := r.Header.Get(h); token != "" { + return a.Authenticate("", token) + } + } + + // 4. Query parameters + for _, p := range ra.TokenQueryParams { + if token := r.URL.Query().Get(p); token != "" { + return a.Authenticate("", token) + } + } + + return nil, ErrNoCredentials +} diff --git a/cmd/auth-proxy/go.mod b/cmd/auth-proxy/go.mod index 11b8e93..8f14406 100644 --- a/cmd/auth-proxy/go.mod +++ b/cmd/auth-proxy/go.mod @@ -4,10 +4,13 @@ go 1.25.0 require ( github.com/joho/godotenv v1.5.1 + github.com/therootcompany/golib/auth v1.0.0 github.com/therootcompany/golib/auth/csvauth v1.2.2 ) -require ( - github.com/therootcompany/golib/auth v1.0.0 // indirect - golang.org/x/crypto v0.42.0 // indirect +require golang.org/x/crypto v0.42.0 // indirect + +replace ( + github.com/therootcompany/golib/auth => ../../auth + github.com/therootcompany/golib/auth/csvauth => ../../auth/csvauth ) diff --git a/cmd/auth-proxy/main.go b/cmd/auth-proxy/main.go index 0e22a3b..ca9df10 100644 --- a/cmd/auth-proxy/main.go +++ b/cmd/auth-proxy/main.go @@ -593,47 +593,21 @@ func matchPattern(grant, rMethod, rHost, rPath string) bool { } func (cli *MainConfig) authenticate(r *http.Request) (auth.BasicPrinciple, error) { - // 1. Try Basic Auth first (cleanest path) - username, password, ok := r.BasicAuth() - if ok { - // Authorization: Basic exists - return creds.Authenticate(username, password) + // nil AuthorizationHeaderSchemes means "not configured" → skip Authorization header. + schemes := cli.AuthorizationHeaderSchemes + if schemes == nil { + schemes = []string{} // non-nil empty slice → skip Authorization header } - - // 2. Any Authorization: - if len(cli.AuthorizationHeaderSchemes) > 0 { - authHeader := r.Header.Get("Authorization") - if authHeader != "" { - parts := strings.SplitN(authHeader, " ", 2) - if len(parts) == 2 { - if cli.AuthorizationHeaderSchemes[0] == "*" || - slices.Contains(cli.AuthorizationHeaderSchemes, parts[0]) { - token := strings.TrimSpace(parts[1]) - // Authorization: exists - return creds.Authenticate(basicAPIKeyName, token) - } - } - return nil, errors.New("'Authorization' header is not properly formatted") - } + ra := auth.RequestAuthenticator{ + AuthorizationSchemes: schemes, + TokenHeaders: cli.TokenHeaderNames, + TokenQueryParams: cli.QueryParamNames, } - - // 3. API-Key / X-API-Key headers - for _, h := range cli.TokenHeaderNames { - if key := r.Header.Get(h); key != "" { - // : exists - return creds.Authenticate(basicAPIKeyName, key) - } + cred, err := ra.Authenticate(r, creds) + if errors.Is(err, auth.ErrNoCredentials) { + return nil, ErrNoAuth } - - // 4. access_token query param - for _, h := range cli.QueryParamNames { - if token := r.URL.Query().Get(h); token != "" { - // ?= exists - return creds.Authenticate(basicAPIKeyName, token) - } - } - - return nil, ErrNoAuth + return cred, err } // peekOption looks for a flag value without parsing the full set diff --git a/cmd/smsapid/go.mod b/cmd/smsapid/go.mod index 0964a7d..681eda8 100644 --- a/cmd/smsapid/go.mod +++ b/cmd/smsapid/go.mod @@ -22,6 +22,7 @@ require ( ) replace ( + github.com/therootcompany/golib/auth => ../../auth github.com/therootcompany/golib/auth/csvauth => ../../auth/csvauth github.com/therootcompany/golib/http/androidsmsgateway => ../../http/androidsmsgateway github.com/therootcompany/golib/http/middleware/v2 => ../../http/middleware diff --git a/cmd/smsapid/main.go b/cmd/smsapid/main.go index 47b75ec..85eb6e0 100644 --- a/cmd/smsapid/main.go +++ b/cmd/smsapid/main.go @@ -35,6 +35,10 @@ var pingWriter jsonl.Writer var smsAuth *csvauth.Auth +var smsRequestAuth = &auth.RequestAuthenticator{ + TokenHeaders: []string{"API-Key", "X-API-Key"}, +} + func main() { jsonf.Indent = 3 @@ -142,7 +146,7 @@ func requireSMSPermission(permission string) func(http.Handler) http.Handler { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - cred, err := authenticateSMS(r) + cred, err := smsRequestAuth.Authenticate(r, smsAuth) if err != nil || !hasSMSPermission(cred.Permissions(), permission) { w.Header().Set("WWW-Authenticate", `Basic`) http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -153,32 +157,6 @@ func requireSMSPermission(permission string) func(http.Handler) http.Handler { } } -// authenticateSMS extracts and verifies credentials from Basic Auth, Authorization header, or API-Key header. -func authenticateSMS(r *http.Request) (auth.BasicPrinciple, error) { - // 1. Basic Auth — Authenticate handles both login credentials and token-as-username/password. - if username, password, ok := r.BasicAuth(); ok { - return smsAuth.Authenticate(username, password) - } - - // 2. Authorization: - if authHeader := r.Header.Get("Authorization"); authHeader != "" { - parts := strings.SplitN(authHeader, " ", 2) - if len(parts) == 2 { - return smsAuth.Authenticate("", strings.TrimSpace(parts[1])) - } - return nil, csvauth.ErrUnauthorized - } - - // 3. API-Key / X-API-Key headers - for _, h := range []string{"API-Key", "X-API-Key"} { - if key := r.Header.Get(h); key != "" { - return smsAuth.Authenticate("", key) - } - } - - return nil, csvauth.ErrNotFound -} - // getAESKey reads an AES-128 key (32 hex chars) from an environment variable. // Returns a zero key if the variable is absent or invalid. func getAESKey(envname string) []byte {