From 3191b3c08c631e3c57d3cbed0ea43e3f1842c23e Mon Sep 17 00:00:00 2001 From: Joe Jose <45399349+joejose97@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:09:31 +0530 Subject: [PATCH] feat(control server authentication): add basic http auth (#2423) --- internal/server/middlewares/auth/basic.go | 36 ++++++++++++++++++++ internal/server/middlewares/auth/lookup.go | 2 ++ internal/server/middlewares/auth/settings.go | 16 +++++++-- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 internal/server/middlewares/auth/basic.go diff --git a/internal/server/middlewares/auth/basic.go b/internal/server/middlewares/auth/basic.go new file mode 100644 index 000000000..3f9da93ea --- /dev/null +++ b/internal/server/middlewares/auth/basic.go @@ -0,0 +1,36 @@ +package auth + +import ( + "crypto/sha256" + "crypto/subtle" + "net/http" +) + +type basicAuthMethod struct { + authDigest [32]byte +} + +func newBasicAuthMethod(username, password string) *basicAuthMethod { + return &basicAuthMethod{ + authDigest: sha256.Sum256([]byte(username + password)), + } +} + +// equal returns true if another auth checker is equal. +// This is used to deduplicate checkers for a particular route. +func (a *basicAuthMethod) equal(other authorizationChecker) bool { + otherBasicMethod, ok := other.(*basicAuthMethod) + if !ok { + return false + } + return a.authDigest == otherBasicMethod.authDigest +} + +func (a *basicAuthMethod) isAuthorized(request *http.Request) bool { + username, password, ok := request.BasicAuth() + if !ok { + return false + } + requestAuthDigest := sha256.Sum256([]byte(username + password)) + return subtle.ConstantTimeCompare(a.authDigest[:], requestAuthDigest[:]) == 1 +} diff --git a/internal/server/middlewares/auth/lookup.go b/internal/server/middlewares/auth/lookup.go index 51323f931..d02c433b3 100644 --- a/internal/server/middlewares/auth/lookup.go +++ b/internal/server/middlewares/auth/lookup.go @@ -18,6 +18,8 @@ func settingsToLookupMap(settings Settings) (routeToRoles map[string][]internalR checker = newNoneMethod() case AuthAPIKey: checker = newAPIKeyMethod(role.APIKey) + case AuthBasic: + checker = newBasicAuthMethod(role.Username, role.Password) default: return nil, fmt.Errorf("%w: %s", ErrMethodNotSupported, role.Auth) } diff --git a/internal/server/middlewares/auth/settings.go b/internal/server/middlewares/auth/settings.go index 93c30fde7..0a9d88837 100644 --- a/internal/server/middlewares/auth/settings.go +++ b/internal/server/middlewares/auth/settings.go @@ -78,6 +78,7 @@ func (s Settings) ToLinesNode() (node *gotree.Node) { const ( AuthNone = "none" AuthAPIKey = "apikey" + AuthBasic = "basic" ) // Role contains the role name, authentication method name and @@ -90,6 +91,10 @@ type Role struct { Auth string // APIKey is the API key to use when using the 'apikey' authentication. APIKey string + // Username for HTTP Basic authentication method. + Username string + // Password for HTTP Basic authentication method. + Password string // Routes is a list of routes that the role can access in the format // "HTTP_METHOD PATH", for example "GET /v1/vpn/status" Routes []string @@ -98,17 +103,24 @@ type Role struct { var ( ErrMethodNotSupported = errors.New("authentication method not supported") ErrAPIKeyEmpty = errors.New("api key is empty") + ErrBasicUsernameEmpty = errors.New("username is empty") + ErrBasicPasswordEmpty = errors.New("password is empty") ErrRouteNotSupported = errors.New("route not supported by the control server") ) func (r Role) validate() (err error) { - err = validate.IsOneOf(r.Auth, AuthNone, AuthAPIKey) + err = validate.IsOneOf(r.Auth, AuthNone, AuthAPIKey, AuthBasic) if err != nil { return fmt.Errorf("%w: %s", ErrMethodNotSupported, r.Auth) } - if r.Auth == AuthAPIKey && r.APIKey == "" { + switch { + case r.Auth == AuthAPIKey && r.APIKey == "": return fmt.Errorf("for role %s: %w", r.Name, ErrAPIKeyEmpty) + case r.Auth == AuthBasic && r.Username == "": + return fmt.Errorf("for role %s: %w", r.Name, ErrBasicUsernameEmpty) + case r.Auth == AuthBasic && r.Password == "": + return fmt.Errorf("for role %s: %w", r.Name, ErrBasicPasswordEmpty) } for i, route := range r.Routes {