From 2e3a07924cf98214952a3fe34aad7a36b2bd2917 Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Fri, 7 Jun 2024 18:28:30 -0400 Subject: [PATCH 1/6] enhance keyauth middleware to handle multile keyExtractors in a simple way Signed-off-by: Dave Lee --- middleware/keyauth/keyauth.go | 62 ++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index ce185240c2..dedb66f0d2 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -19,23 +19,37 @@ const ( cookie = "cookie" ) +type extractorFunc func(c *fiber.Ctx) (string, error) + // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Init config cfg := configDefault(config...) // Initialize - parts := strings.Split(cfg.KeyLookup, ":") - extractor := keyFromHeader(parts[1], cfg.AuthScheme) - switch parts[0] { - case query: - extractor = keyFromQuery(parts[1]) - case form: - extractor = keyFromForm(parts[1]) - case param: - extractor = keyFromParam(parts[1]) - case cookie: - extractor = keyFromCookie(parts[1]) + + parts := strings.Split(cfg.KeyLookup, "|") + + var extractor extractorFunc + if len(parts) <= 1 { + extractor = parseSingleExtractor(cfg.KeyLookup, cfg.AuthScheme) + } else { + subExtractors := []extractorFunc{} + for _, keyLookup := range parts { + subExtractors = append(subExtractors, parseSingleExtractor(keyLookup, cfg.AuthScheme)) + } + extractor = func(c *fiber.Ctx) (string, error) { + for _, subExtractor := range subExtractors { + res, err := subExtractor(c) + if err == nil && res != "" { + return res, nil + } + if !errors.Is(err, ErrMissingOrMalformedAPIKey) { + return "", err + } + } + return "", ErrMissingOrMalformedAPIKey + } } // Return middleware handler @@ -61,8 +75,24 @@ func New(config ...Config) fiber.Handler { } } +func parseSingleExtractor(keyLookup string, authScheme string) extractorFunc { + parts := strings.Split(keyLookup, ":") + extractor := keyFromHeader(parts[1], authScheme) + switch parts[0] { + case query: + extractor = keyFromQuery(parts[1]) + case form: + extractor = keyFromForm(parts[1]) + case param: + extractor = keyFromParam(parts[1]) + case cookie: + extractor = keyFromCookie(parts[1]) + } + return extractor +} + // keyFromHeader returns a function that extracts api key from the request header. -func keyFromHeader(header, authScheme string) func(c *fiber.Ctx) (string, error) { +func keyFromHeader(header, authScheme string) extractorFunc { return func(c *fiber.Ctx) (string, error) { auth := c.Get(header) l := len(authScheme) @@ -77,7 +107,7 @@ func keyFromHeader(header, authScheme string) func(c *fiber.Ctx) (string, error) } // keyFromQuery returns a function that extracts api key from the query string. -func keyFromQuery(param string) func(c *fiber.Ctx) (string, error) { +func keyFromQuery(param string) extractorFunc { return func(c *fiber.Ctx) (string, error) { key := c.Query(param) if key == "" { @@ -88,7 +118,7 @@ func keyFromQuery(param string) func(c *fiber.Ctx) (string, error) { } // keyFromForm returns a function that extracts api key from the form. -func keyFromForm(param string) func(c *fiber.Ctx) (string, error) { +func keyFromForm(param string) extractorFunc { return func(c *fiber.Ctx) (string, error) { key := c.FormValue(param) if key == "" { @@ -99,7 +129,7 @@ func keyFromForm(param string) func(c *fiber.Ctx) (string, error) { } // keyFromParam returns a function that extracts api key from the url param string. -func keyFromParam(param string) func(c *fiber.Ctx) (string, error) { +func keyFromParam(param string) extractorFunc { return func(c *fiber.Ctx) (string, error) { key, err := url.PathUnescape(c.Params(param)) if err != nil { @@ -110,7 +140,7 @@ func keyFromParam(param string) func(c *fiber.Ctx) (string, error) { } // keyFromCookie returns a function that extracts api key from the named cookie. -func keyFromCookie(name string) func(c *fiber.Ctx) (string, error) { +func keyFromCookie(name string) extractorFunc { return func(c *fiber.Ctx) (string, error) { key := c.Cookies(name) if key == "" { From 951b11b521c15b2e4f4da990c0dcf385e9293351 Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Sat, 8 Jun 2024 03:31:47 -0400 Subject: [PATCH 2/6] update docs slightly Signed-off-by: Dave Lee --- docs/api/middleware/keyauth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/middleware/keyauth.md b/docs/api/middleware/keyauth.md index 4705c2e667..1c404de052 100644 --- a/docs/api/middleware/keyauth.md +++ b/docs/api/middleware/keyauth.md @@ -218,7 +218,7 @@ curl --header "Authorization: Bearer my-super-secret-key" http://localhost:3000 | Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | | SuccessHandler | `fiber.Handler` | SuccessHandler defines a function which is executed for a valid key. | `nil` | | ErrorHandler | `fiber.ErrorHandler` | ErrorHandler defines a function which is executed for an invalid key. | `401 Invalid or expired key` | -| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract key from the request. | "header:Authorization" | +| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract key from the request. If multiple keys are required, they can be delimited with a pipe (`|`) character, and the first matching key will be selected. | "header:Authorization" | | AuthScheme | `string` | AuthScheme to be used in the Authorization header. | "Bearer" | | Validator | `func(*fiber.Ctx, string) (bool, error)` | Validator is a function to validate the key. | A function for key validation | | ContextKey | `interface{}` | Context key to store the bearer token from the token into context. | "token" | From 2eee0dd61e3b4dfde22e21191d530a5647f085a1 Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Sat, 8 Jun 2024 03:49:50 -0400 Subject: [PATCH 3/6] add unit test Signed-off-by: Dave Lee --- middleware/keyauth/keyauth_test.go | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index 9d9b3395da..fb9e54de93 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -131,6 +131,51 @@ func TestAuthSources(t *testing.T) { } } +func TestMultipleKeyLookup(t *testing.T) { + const ( + desc = "auth with correct key" + success = "Success!" + ) + + // setup the fiber endpoint + app := fiber.New() + authMiddleware := New(Config{ + KeyLookup: "header:key|query:key", + Validator: func(c *fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + }) + app.Use(authMiddleware) + app.Get("/foo", func(c *fiber.Ctx) error { + return c.SendString(success) + }) + + // construct the test HTTP request + var req *http.Request + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/foo", nil) + utils.AssertEqual(t, err, nil) + q := req.URL.Query() + q.Add("key", CorrectKey) + req.URL.RawQuery = q.Encode() + + res, err := app.Test(req, -1) + + utils.AssertEqual(t, nil, err, desc) + + // test the body of the request + body, err := io.ReadAll(res.Body) + utils.AssertEqual(t, 200, res.StatusCode, desc) + // body + utils.AssertEqual(t, nil, err, desc) + utils.AssertEqual(t, success, string(body), desc) + + err = res.Body.Close() + utils.AssertEqual(t, err, nil) +} + func TestMultipleKeyAuth(t *testing.T) { // setup the fiber endpoint app := fiber.New() From 8659bd163efe2111a2ce0b4b2d1f1bddf6d76d92 Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Sun, 9 Jun 2024 01:29:57 -0400 Subject: [PATCH 4/6] replace string splitting with an explicit parameter Signed-off-by: Dave Lee --- docs/api/middleware/keyauth.md | 3 ++- middleware/keyauth/config.go | 7 +++++++ middleware/keyauth/keyauth.go | 33 ++++++++++++++++++------------ middleware/keyauth/keyauth_test.go | 3 ++- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/docs/api/middleware/keyauth.md b/docs/api/middleware/keyauth.md index 1c404de052..df23f632c4 100644 --- a/docs/api/middleware/keyauth.md +++ b/docs/api/middleware/keyauth.md @@ -218,7 +218,8 @@ curl --header "Authorization: Bearer my-super-secret-key" http://localhost:3000 | Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | | SuccessHandler | `fiber.Handler` | SuccessHandler defines a function which is executed for a valid key. | `nil` | | ErrorHandler | `fiber.ErrorHandler` | ErrorHandler defines a function which is executed for an invalid key. | `401 Invalid or expired key` | -| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract key from the request. If multiple keys are required, they can be delimited with a pipe (`|`) character, and the first matching key will be selected. | "header:Authorization" | +| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract key from the request. | "header:Authorization" | +| AdditionalKeyLookups | `[]string` | If additional fallback sources of keys are required, they can be specified here in order of precedence | []string{} (empty) | | AuthScheme | `string` | AuthScheme to be used in the Authorization header. | "Bearer" | | Validator | `func(*fiber.Ctx, string) (bool, error)` | Validator is a function to validate the key. | A function for key validation | | ContextKey | `interface{}` | Context key to store the bearer token from the token into context. | "token" | diff --git a/middleware/keyauth/config.go b/middleware/keyauth/config.go index c762d72ce6..731497172d 100644 --- a/middleware/keyauth/config.go +++ b/middleware/keyauth/config.go @@ -32,6 +32,10 @@ type Config struct { // - "cookie:" KeyLookup string + // AdditionalKeyLookups is a slice of strings, containing secondary sources of keys if KeyLookup does not find one + // Each element should be a value used in KeyLookup + AdditionalKeyLookups []string + // AuthScheme to be used in the Authorization header. // Optional. Default value "Bearer". AuthScheme string @@ -84,6 +88,9 @@ func configDefault(config ...Config) Config { cfg.AuthScheme = ConfigDefault.AuthScheme } } + if cfg.AdditionalKeyLookups == nil { + cfg.AdditionalKeyLookups = []string{} + } if cfg.Validator == nil { panic("fiber: keyauth middleware requires a validator function") } diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index dedb66f0d2..0dc054aaae 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -3,6 +3,7 @@ package keyauth import ( "errors" + "fmt" "net/url" "strings" @@ -28,24 +29,27 @@ func New(config ...Config) fiber.Handler { // Initialize - parts := strings.Split(cfg.KeyLookup, "|") - var extractor extractorFunc - if len(parts) <= 1 { - extractor = parseSingleExtractor(cfg.KeyLookup, cfg.AuthScheme) - } else { - subExtractors := []extractorFunc{} - for _, keyLookup := range parts { - subExtractors = append(subExtractors, parseSingleExtractor(keyLookup, cfg.AuthScheme)) + extractor, err := parseSingleExtractor(cfg.KeyLookup, cfg.AuthScheme) + if err != nil { + panic(fmt.Errorf("error creating middleware: invalid keyauth Config.KeyLookup: %w", err)) + } + if len(cfg.AdditionalKeyLookups) > 0 { + subExtractors := map[string]extractorFunc{cfg.KeyLookup: extractor} + for _, keyLookup := range cfg.AdditionalKeyLookups { + subExtractors[keyLookup], err = parseSingleExtractor(keyLookup, cfg.AuthScheme) + if err != nil { + panic(fmt.Errorf("error creating middleware: invalid keyauth Config.AdditionalKeyLookups[%s]: %w", keyLookup, err)) + } } extractor = func(c *fiber.Ctx) (string, error) { - for _, subExtractor := range subExtractors { + for keyLookup, subExtractor := range subExtractors { res, err := subExtractor(c) if err == nil && res != "" { return res, nil } if !errors.Is(err, ErrMissingOrMalformedAPIKey) { - return "", err + return "", fmt.Errorf("[%s] %w", keyLookup, err) } } return "", ErrMissingOrMalformedAPIKey @@ -75,9 +79,12 @@ func New(config ...Config) fiber.Handler { } } -func parseSingleExtractor(keyLookup string, authScheme string) extractorFunc { +func parseSingleExtractor(keyLookup string, authScheme string) (extractorFunc, error) { parts := strings.Split(keyLookup, ":") - extractor := keyFromHeader(parts[1], authScheme) + if len(parts) <= 1 { + return nil, fmt.Errorf("invalid keyLookup") + } + extractor := keyFromHeader(parts[1], authScheme) // in the event of an invalid prefix, it is interpreted as header: switch parts[0] { case query: extractor = keyFromQuery(parts[1]) @@ -88,7 +95,7 @@ func parseSingleExtractor(keyLookup string, authScheme string) extractorFunc { case cookie: extractor = keyFromCookie(parts[1]) } - return extractor + return extractor, nil } // keyFromHeader returns a function that extracts api key from the request header. diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index fb9e54de93..0e4ee20e26 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -140,7 +140,8 @@ func TestMultipleKeyLookup(t *testing.T) { // setup the fiber endpoint app := fiber.New() authMiddleware := New(Config{ - KeyLookup: "header:key|query:key", + KeyLookup: "header:key", + AdditionalKeyLookups: []string{"cookie:key", "query:key"}, Validator: func(c *fiber.Ctx, key string) (bool, error) { if key == CorrectKey { return true, nil From 3e512454f34fc5035f077f5e33b011b690df94fc Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Sun, 9 Jun 2024 04:07:41 -0400 Subject: [PATCH 5/6] gofumpt Signed-off-by: Dave Lee --- middleware/keyauth/keyauth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index 0dc054aaae..5958eed9dc 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -79,7 +79,7 @@ func New(config ...Config) fiber.Handler { } } -func parseSingleExtractor(keyLookup string, authScheme string) (extractorFunc, error) { +func parseSingleExtractor(keyLookup, authScheme string) (extractorFunc, error) { parts := strings.Split(keyLookup, ":") if len(parts) <= 1 { return nil, fmt.Errorf("invalid keyLookup") From 468bd1e73170de197b2fd66f332db150c0396d6d Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Sun, 9 Jun 2024 13:29:27 -0400 Subject: [PATCH 6/6] address comments Signed-off-by: Dave Lee --- docs/api/middleware/keyauth.md | 3 ++- middleware/keyauth/config.go | 8 ++++---- middleware/keyauth/keyauth.go | 6 +++--- middleware/keyauth/keyauth_test.go | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/api/middleware/keyauth.md b/docs/api/middleware/keyauth.md index df23f632c4..c198bf4410 100644 --- a/docs/api/middleware/keyauth.md +++ b/docs/api/middleware/keyauth.md @@ -219,7 +219,7 @@ curl --header "Authorization: Bearer my-super-secret-key" http://localhost:3000 | SuccessHandler | `fiber.Handler` | SuccessHandler defines a function which is executed for a valid key. | `nil` | | ErrorHandler | `fiber.ErrorHandler` | ErrorHandler defines a function which is executed for an invalid key. | `401 Invalid or expired key` | | KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract key from the request. | "header:Authorization" | -| AdditionalKeyLookups | `[]string` | If additional fallback sources of keys are required, they can be specified here in order of precedence | []string{} (empty) | +| FallbackKeyLookups | `[]string` | If additional fallback sources of keys are required, they can be specified here in order of precedence | []string{} (empty) | | AuthScheme | `string` | AuthScheme to be used in the Authorization header. | "Bearer" | | Validator | `func(*fiber.Ctx, string) (bool, error)` | Validator is a function to validate the key. | A function for key validation | | ContextKey | `interface{}` | Context key to store the bearer token from the token into context. | "token" | @@ -238,6 +238,7 @@ var ConfigDefault = Config{ return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired API Key") }, KeyLookup: "header:" + fiber.HeaderAuthorization, + FallbackKeyLookups: []string{}, AuthScheme: "Bearer", ContextKey: "token", } diff --git a/middleware/keyauth/config.go b/middleware/keyauth/config.go index 731497172d..add9bf44ce 100644 --- a/middleware/keyauth/config.go +++ b/middleware/keyauth/config.go @@ -32,9 +32,9 @@ type Config struct { // - "cookie:" KeyLookup string - // AdditionalKeyLookups is a slice of strings, containing secondary sources of keys if KeyLookup does not find one + // FallbackKeyLookups is a slice of strings, containing secondary sources of keys if KeyLookup does not find one // Each element should be a value used in KeyLookup - AdditionalKeyLookups []string + FallbackKeyLookups []string // AuthScheme to be used in the Authorization header. // Optional. Default value "Bearer". @@ -88,8 +88,8 @@ func configDefault(config ...Config) Config { cfg.AuthScheme = ConfigDefault.AuthScheme } } - if cfg.AdditionalKeyLookups == nil { - cfg.AdditionalKeyLookups = []string{} + if cfg.FallbackKeyLookups == nil { + cfg.FallbackKeyLookups = []string{} } if cfg.Validator == nil { panic("fiber: keyauth middleware requires a validator function") diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index 5958eed9dc..df89f18e0f 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -34,12 +34,12 @@ func New(config ...Config) fiber.Handler { if err != nil { panic(fmt.Errorf("error creating middleware: invalid keyauth Config.KeyLookup: %w", err)) } - if len(cfg.AdditionalKeyLookups) > 0 { + if len(cfg.FallbackKeyLookups) > 0 { subExtractors := map[string]extractorFunc{cfg.KeyLookup: extractor} - for _, keyLookup := range cfg.AdditionalKeyLookups { + for _, keyLookup := range cfg.FallbackKeyLookups { subExtractors[keyLookup], err = parseSingleExtractor(keyLookup, cfg.AuthScheme) if err != nil { - panic(fmt.Errorf("error creating middleware: invalid keyauth Config.AdditionalKeyLookups[%s]: %w", keyLookup, err)) + panic(fmt.Errorf("error creating middleware: invalid keyauth Config.FallbackKeyLookups[%s]: %w", keyLookup, err)) } } extractor = func(c *fiber.Ctx) (string, error) { diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index 0e4ee20e26..00eae30665 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -140,8 +140,8 @@ func TestMultipleKeyLookup(t *testing.T) { // setup the fiber endpoint app := fiber.New() authMiddleware := New(Config{ - KeyLookup: "header:key", - AdditionalKeyLookups: []string{"cookie:key", "query:key"}, + KeyLookup: "header:key", + FallbackKeyLookups: []string{"cookie:key", "query:key"}, Validator: func(c *fiber.Ctx, key string) (bool, error) { if key == CorrectKey { return true, nil