golib/auth/request.go
copilot-swe-agent[bot] 9b79800b07 refactor: remove Challenge() method; use w.Header().Set("WWW-Authenticate", ...) directly
Co-authored-by: coolaj86 <122831+coolaj86@users.noreply.github.com>
2026-03-02 08:10:17 +00:00

116 lines
3.8 KiB
Go

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.
//
// Use NewRequestAuthenticator for sane defaults.
type RequestAuthenticator struct {
// Authenticator is the credential verifier called with the extracted
// username/password or token. Must be set before calling Authenticate.
Authenticator BasicAuthenticator
// AuthenticateBasic enables HTTP Basic Auth (Authorization: Basic …).
AuthenticateBasic bool
// AuthorizationSchemes lists accepted schemes for "Authorization: <scheme> <token>".
// 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{"API-Key", "X-API-Key"}.
TokenHeaders []string
// TokenQueryParams lists query parameter names checked for tokens,
// e.g. []string{"access_token", "token"}.
TokenQueryParams []string
// WWWAuthenticate 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.
// NewRequestAuthenticator sets this to "Basic".
//
// Example:
//
// if ra.WWWAuthenticate != "" {
// w.Header().Set("WWW-Authenticate", ra.WWWAuthenticate)
// }
WWWAuthenticate string
}
// NewRequestAuthenticator returns a RequestAuthenticator with sane defaults:
// Basic Auth enabled, Bearer/Token Authorization schemes, common API-key
// headers, access_token/token query params, and WWWAuthenticate "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",
}
}
// Authenticate extracts credentials from r in this order:
// 1. Basic Auth (Authorization: Basic …)
// 2. Authorization: <scheme> <token> (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) (BasicPrinciple, error) {
a := ra.Authenticator
// 1. Basic Auth
if ra.AuthenticateBasic {
if username, password, ok := r.BasicAuth(); ok {
return a.Authenticate(username, password)
}
}
// 2. Authorization: <scheme> <token>
// 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
}