diff --git a/auth/request.go b/auth/request.go new file mode 100644 index 0000000..236f1af --- /dev/null +++ b/auth/request.go @@ -0,0 +1,138 @@ +// BasicRequestAuthenticator - Authenticate HTTP requests by the most common methods +// +// Authored in 2026 by AJ ONeal , assisted by GitHub Copilot (Claude). +// To the extent possible under law, the author(s) have dedicated all copyright +// and related and neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along with +// this software. If not, see . +// +// SPDX-License-Identifier: CC0-1.0 + +package auth + +import ( + "errors" + "net/http" + "slices" + "strings" +) + +// ErrNoCredentials is returned by BasicRequestAuthenticator.Authenticate when the +// request contains no recognizable form of credentials. +var ErrNoCredentials = errors.New("no credentials provided") + +// BasicRequestAuthenticator 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. +// +// Use NewBasicRequestAuthenticator for sane defaults. +type BasicRequestAuthenticator struct { + // BasicAuth enables HTTP Basic Auth (Authorization: Basic …). + BasicAuth bool + + // BasicRealm is the suggested value for the WWW-Authenticate response + // header. Set it on the response before writing a 401 Unauthorized so that + // clients know which auth scheme to use. An empty string means no header. + // NewBasicRequestAuthenticator sets this to "Basic". + // + // Example: + // + // if _, err := ra.Authenticate(r); err != nil { + // w.Header().Set("WWW-Authenticate", ra.BasicRealm) + // http.Error(w, "Unauthorized", http.StatusUnauthorized) + // return + // } + BasicRealm string + + // Authenticator is the credential verifier called with the extracted + // username/password or token. Must be set before calling Authenticate. + Authenticator BasicAuthenticator + + // AuthorizationSchemes lists accepted schemes for "Authorization: ". + // nil or an empty slice skips the Authorization header entirely; + // ["*"] accepts any scheme; ["Bearer", "Token"] restricts to those schemes. + AuthorizationSchemes []string + + // TokenHeaders lists header names checked for bearer tokens, + // e.g. []string{"X-API-Key"}. + TokenHeaders []string + + // TokenQueryParams lists query parameter names checked for tokens, + // e.g. []string{"access_token", "token"}. + TokenQueryParams []string +} + +// NewBasicRequestAuthenticator returns a BasicRequestAuthenticator with sane defaults: +// Basic Auth enabled, Bearer/Token Authorization schemes, common API-key +// headers, access_token/token query params, and BasicRealm "Basic". +// +// Example: +// +// cred, err := ra.Authenticate(r) +// if err != nil { +// w.Header().Set("WWW-Authenticate", ra.BasicRealm) +// http.Error(w, "Unauthorized", http.StatusUnauthorized) +// return +// } +func NewBasicRequestAuthenticator() *BasicRequestAuthenticator { + return &BasicRequestAuthenticator{ + BasicAuth: true, + BasicRealm: "Basic", + AuthorizationSchemes: []string{"Bearer", "Token"}, + TokenHeaders: []string{"X-API-Key"}, + TokenQueryParams: []string{"access_token", "token"}, + } +} + +// 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 *BasicRequestAuthenticator) Authenticate(r *http.Request) (BasicPrinciple, error) { + a := ra.Authenticator + + // 1. Basic Auth + if ra.BasicAuth { + if username, password, ok := r.BasicAuth(); ok { + return a.Authenticate(username, password) + } + } + + // 2. Authorization: + // AuthorizationSchemes must be non-empty to check the Authorization header; + // nil or empty skips it entirely. + if 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[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/auth/request_example_test.go b/auth/request_example_test.go new file mode 100644 index 0000000..7861354 --- /dev/null +++ b/auth/request_example_test.go @@ -0,0 +1,35 @@ +package auth_test + +import ( + "fmt" + "net/http" + + "github.com/therootcompany/golib/auth" +) + +// exampleCredentialStore is a toy BasicAuthenticator used only in the example below. +type exampleCredentialStore struct{} + +func (exampleCredentialStore) Authenticate(username, password string) (auth.BasicPrinciple, error) { + return nil, fmt.Errorf("not implemented") +} + +// ExampleBasicRequestAuthenticator shows the typical usage pattern. +// Build a BasicRequestAuthenticator once (at startup), attach your credential +// store as the Authenticator, then call Authenticate in each handler. +// Set the WWW-Authenticate header before writing a 401 to instruct the browser +// to prompt for Username and Password on failure. +func ExampleBasicRequestAuthenticator() { + ra := auth.NewBasicRequestAuthenticator() + ra.Authenticator = exampleCredentialStore{} // swap in your real credential store + + http.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) { + principle, err := ra.Authenticate(r) + if err != nil { + w.Header().Set("WWW-Authenticate", ra.BasicRealm) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + fmt.Fprintf(w, "hello %s", principle.ID()) + }) +}