mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-02 23:57:59 +00:00
refactor: extract RequestAuthenticator into auth package; use in smsapid and auth-proxy
Co-authored-by: coolaj86 <122831+coolaj86@users.noreply.github.com>
This commit is contained in:
parent
07adfb08c7
commit
c8eabab03d
77
auth/request.go
Normal file
77
auth/request.go
Normal file
@ -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: <scheme> <token>".
|
||||||
|
// 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: <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, a BasicAuthenticator) (BasicPrinciple, error) {
|
||||||
|
// 1. Basic Auth
|
||||||
|
if username, password, ok := r.BasicAuth(); ok {
|
||||||
|
return a.Authenticate(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Authorization: <scheme> <token>
|
||||||
|
// 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
|
||||||
|
}
|
||||||
@ -4,10 +4,13 @@ go 1.25.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/therootcompany/golib/auth v1.0.0
|
||||||
github.com/therootcompany/golib/auth/csvauth v1.2.2
|
github.com/therootcompany/golib/auth/csvauth v1.2.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require golang.org/x/crypto v0.42.0 // indirect
|
||||||
github.com/therootcompany/golib/auth v1.0.0 // indirect
|
|
||||||
golang.org/x/crypto v0.42.0 // indirect
|
replace (
|
||||||
|
github.com/therootcompany/golib/auth => ../../auth
|
||||||
|
github.com/therootcompany/golib/auth/csvauth => ../../auth/csvauth
|
||||||
)
|
)
|
||||||
|
|||||||
@ -593,48 +593,22 @@ func matchPattern(grant, rMethod, rHost, rPath string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cli *MainConfig) authenticate(r *http.Request) (auth.BasicPrinciple, error) {
|
func (cli *MainConfig) authenticate(r *http.Request) (auth.BasicPrinciple, error) {
|
||||||
// 1. Try Basic Auth first (cleanest path)
|
// nil AuthorizationHeaderSchemes means "not configured" → skip Authorization header.
|
||||||
username, password, ok := r.BasicAuth()
|
schemes := cli.AuthorizationHeaderSchemes
|
||||||
if ok {
|
if schemes == nil {
|
||||||
// Authorization: Basic <Auth> exists
|
schemes = []string{} // non-nil empty slice → skip Authorization header
|
||||||
return creds.Authenticate(username, password)
|
|
||||||
}
|
}
|
||||||
|
ra := auth.RequestAuthenticator{
|
||||||
// 2. Any Authorization: <scheme> <token>
|
AuthorizationSchemes: schemes,
|
||||||
if len(cli.AuthorizationHeaderSchemes) > 0 {
|
TokenHeaders: cli.TokenHeaderNames,
|
||||||
authHeader := r.Header.Get("Authorization")
|
TokenQueryParams: cli.QueryParamNames,
|
||||||
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: <Scheme> <Token> exists
|
|
||||||
return creds.Authenticate(basicAPIKeyName, token)
|
|
||||||
}
|
}
|
||||||
}
|
cred, err := ra.Authenticate(r, creds)
|
||||||
return nil, errors.New("'Authorization' header is not properly formatted")
|
if errors.Is(err, auth.ErrNoCredentials) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. API-Key / X-API-Key headers
|
|
||||||
for _, h := range cli.TokenHeaderNames {
|
|
||||||
if key := r.Header.Get(h); key != "" {
|
|
||||||
// <TokenHeader>: <Token> exists
|
|
||||||
return creds.Authenticate(basicAPIKeyName, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. access_token query param
|
|
||||||
for _, h := range cli.QueryParamNames {
|
|
||||||
if token := r.URL.Query().Get(h); token != "" {
|
|
||||||
// <query_param>?=<Token> exists
|
|
||||||
return creds.Authenticate(basicAPIKeyName, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrNoAuth
|
return nil, ErrNoAuth
|
||||||
}
|
}
|
||||||
|
return cred, err
|
||||||
|
}
|
||||||
|
|
||||||
// peekOption looks for a flag value without parsing the full set
|
// peekOption looks for a flag value without parsing the full set
|
||||||
func peekOption(args []string, names []string, def string) string {
|
func peekOption(args []string, names []string, def string) string {
|
||||||
|
|||||||
@ -22,6 +22,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
|
github.com/therootcompany/golib/auth => ../../auth
|
||||||
github.com/therootcompany/golib/auth/csvauth => ../../auth/csvauth
|
github.com/therootcompany/golib/auth/csvauth => ../../auth/csvauth
|
||||||
github.com/therootcompany/golib/http/androidsmsgateway => ../../http/androidsmsgateway
|
github.com/therootcompany/golib/http/androidsmsgateway => ../../http/androidsmsgateway
|
||||||
github.com/therootcompany/golib/http/middleware/v2 => ../../http/middleware
|
github.com/therootcompany/golib/http/middleware/v2 => ../../http/middleware
|
||||||
|
|||||||
@ -35,6 +35,10 @@ var pingWriter jsonl.Writer
|
|||||||
|
|
||||||
var smsAuth *csvauth.Auth
|
var smsAuth *csvauth.Auth
|
||||||
|
|
||||||
|
var smsRequestAuth = &auth.RequestAuthenticator{
|
||||||
|
TokenHeaders: []string{"API-Key", "X-API-Key"},
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
jsonf.Indent = 3
|
jsonf.Indent = 3
|
||||||
|
|
||||||
@ -142,7 +146,7 @@ func requireSMSPermission(permission string) func(http.Handler) http.Handler {
|
|||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cred, err := authenticateSMS(r)
|
cred, err := smsRequestAuth.Authenticate(r, smsAuth)
|
||||||
if err != nil || !hasSMSPermission(cred.Permissions(), permission) {
|
if err != nil || !hasSMSPermission(cred.Permissions(), permission) {
|
||||||
w.Header().Set("WWW-Authenticate", `Basic`)
|
w.Header().Set("WWW-Authenticate", `Basic`)
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
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: <scheme> <token>
|
|
||||||
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.
|
// getAESKey reads an AES-128 key (32 hex chars) from an environment variable.
|
||||||
// Returns a zero key if the variable is absent or invalid.
|
// Returns a zero key if the variable is absent or invalid.
|
||||||
func getAESKey(envname string) []byte {
|
func getAESKey(envname string) []byte {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user