From 47b7b55fd4ebc433e47eeacf4d3f014715fb5c0e Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Wed, 11 Sep 2024 09:34:35 +0200 Subject: [PATCH] feat(server): add apikey auth method (#2437) --- internal/server/middlewares/auth/apikey.go | 34 +++++++++++++++++++ .../middlewares/auth/configfile_test.go | 14 +++++++- internal/server/middlewares/auth/lookup.go | 2 ++ internal/server/middlewares/auth/settings.go | 14 ++++++-- 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 internal/server/middlewares/auth/apikey.go diff --git a/internal/server/middlewares/auth/apikey.go b/internal/server/middlewares/auth/apikey.go new file mode 100644 index 000000000..b9a3d8dcd --- /dev/null +++ b/internal/server/middlewares/auth/apikey.go @@ -0,0 +1,34 @@ +package auth + +import ( + "crypto/subtle" + "net/http" +) + +type apiKeyMethod struct { + apiKey string +} + +func newAPIKeyMethod(apiKey string) *apiKeyMethod { + return &apiKeyMethod{ + apiKey: apiKey, + } +} + +// equal returns true if another auth checker is equal. +// This is used to deduplicate checkers for a particular route. +func (a *apiKeyMethod) equal(other authorizationChecker) bool { + otherTokenMethod, ok := other.(*apiKeyMethod) + if !ok { + return false + } + return a.apiKey == otherTokenMethod.apiKey +} + +func (a *apiKeyMethod) isAuthorized(request *http.Request) bool { + xAPIKey := request.Header.Get("X-API-Key") + if xAPIKey == "" { + xAPIKey = request.URL.Query().Get("api_key") + } + return subtle.ConstantTimeCompare([]byte(xAPIKey), []byte(a.apiKey)) == 1 +} diff --git a/internal/server/middlewares/auth/configfile_test.go b/internal/server/middlewares/auth/configfile_test.go index 0123cbb18..4dcc30097 100644 --- a/internal/server/middlewares/auth/configfile_test.go +++ b/internal/server/middlewares/auth/configfile_test.go @@ -33,12 +33,24 @@ func Test_Read(t *testing.T) { fileContent: `[[roles]] name = "public" auth = "none" -routes = ["GET /v1/vpn/status", "PUT /v1/vpn/status"]`, +routes = ["GET /v1/vpn/status", "PUT /v1/vpn/status"] + +[[roles]] +name = "client" +auth = "apikey" +apikey = "xyz" +routes = ["GET /v1/vpn/status"] +`, settings: Settings{ Roles: []Role{{ Name: "public", Auth: AuthNone, Routes: []string{"GET /v1/vpn/status", "PUT /v1/vpn/status"}, + }, { + Name: "client", + Auth: AuthAPIKey, + APIKey: "xyz", + Routes: []string{"GET /v1/vpn/status"}, }}, }, }, diff --git a/internal/server/middlewares/auth/lookup.go b/internal/server/middlewares/auth/lookup.go index e725fd369..51323f931 100644 --- a/internal/server/middlewares/auth/lookup.go +++ b/internal/server/middlewares/auth/lookup.go @@ -16,6 +16,8 @@ func settingsToLookupMap(settings Settings) (routeToRoles map[string][]internalR switch role.Auth { case AuthNone: checker = newNoneMethod() + case AuthAPIKey: + checker = newAPIKeyMethod(role.APIKey) 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 6f3541bec..93c30fde7 100644 --- a/internal/server/middlewares/auth/settings.go +++ b/internal/server/middlewares/auth/settings.go @@ -76,7 +76,8 @@ func (s Settings) ToLinesNode() (node *gotree.Node) { } const ( - AuthNone = "none" + AuthNone = "none" + AuthAPIKey = "apikey" ) // Role contains the role name, authentication method name and @@ -85,8 +86,10 @@ type Role struct { // Name is the role name and is only used for documentation // and in the authentication middleware debug logs. Name string - // Auth is the authentication method to use, which can be 'none'. + // Auth is the authentication method to use, which can be 'none' or 'apikey'. Auth string + // APIKey is the API key to use when using the 'apikey' authentication. + APIKey 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 @@ -94,15 +97,20 @@ type Role struct { var ( ErrMethodNotSupported = errors.New("authentication method not supported") + ErrAPIKeyEmpty = errors.New("api key is empty") ErrRouteNotSupported = errors.New("route not supported by the control server") ) func (r Role) validate() (err error) { - err = validate.IsOneOf(r.Auth, AuthNone) + err = validate.IsOneOf(r.Auth, AuthNone, AuthAPIKey) if err != nil { return fmt.Errorf("%w: %s", ErrMethodNotSupported, r.Auth) } + if r.Auth == AuthAPIKey && r.APIKey == "" { + return fmt.Errorf("for role %s: %w", r.Name, ErrAPIKeyEmpty) + } + for i, route := range r.Routes { _, ok := validRoutes[route] if !ok {