Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔥 Feature: Add support for custom KeyLookup functions in the Keyauth middleware #3028

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0b6b5e9
port over FallbackKeyLookups from v2 middleware to v3
dave-gray101 Jun 9, 2024
f118663
bot pointed out that I missed the format variable
dave-gray101 Jun 9, 2024
a432b80
fix lint and gofumpt issues
dave-gray101 Jun 10, 2024
4e061aa
major revision: instead of FallbackKeyLookups, expose CustomKeyLookup…
dave-gray101 Jun 10, 2024
397ff49
add more tests to boost coverage
dave-gray101 Jun 10, 2024
2968439
teardown code and cleanup
dave-gray101 Jun 10, 2024
5b24181
Merge branch 'main' into feat-keyauth-fallback-keylookup
gaby Jun 11, 2024
7bfc96d
test fixes
dave-gray101 Jun 11, 2024
8c13f25
Merge branch 'main' into feat-keyauth-fallback-keylookup
dave-gray101 Jun 12, 2024
7191f65
slight boost to test coverage
dave-gray101 Jun 16, 2024
1004aa0
Merge branch 'feat-keyauth-fallback-keylookup' of ghgray101:dave-gray…
dave-gray101 Jun 16, 2024
961e8de
docs: fix md table alignment
sixcolors Jun 16, 2024
293c01b
fix comments - change some names, expose functions, improve docs
dave-gray101 Jun 16, 2024
825e11a
Merge branch 'feat-keyauth-fallback-keylookup' of ghgray101:dave-gray…
dave-gray101 Jun 16, 2024
26bc132
missed one old name
dave-gray101 Jun 16, 2024
9588706
fix some suggestions from the bot - error messages, test coverage, ma…
dave-gray101 Jun 17, 2024
2711dc3
Merge branch 'main' into feat-keyauth-fallback-keylookup
gaby Jun 17, 2024
4da76e4
Merge branch 'main' into feat-keyauth-fallback-keylookup
dave-gray101 Jun 18, 2024
12eeca8
Merge branch 'main' into feat-keyauth-fallback-keylookup
dave-gray101 Jun 20, 2024
d6d5bfe
Merge branch 'main' into feat-keyauth-fallback-keylookup
ReneWerner87 Jun 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/middleware/keyauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,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 "`<source>:<name>`" that is used to extract key from the request. | "header:Authorization" |
| 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 |

Expand All @@ -237,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",
}
```
12 changes: 10 additions & 2 deletions middleware/keyauth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ type Config struct {
// - "cookie:<name>"
KeyLookup string

// 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
FallbackKeyLookups []string

// AuthScheme to be used in the Authorization header.
// Optional. Default value "Bearer".
AuthScheme string
Expand All @@ -51,8 +55,9 @@ var ConfigDefault = Config{
}
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired API Key")
},
KeyLookup: "header:" + fiber.HeaderAuthorization,
AuthScheme: "Bearer",
KeyLookup: "header:" + fiber.HeaderAuthorization,
FallbackKeyLookups: []string{},
AuthScheme: "Bearer",
}

// Helper function to set default values
Expand All @@ -79,6 +84,9 @@ func configDefault(config ...Config) Config {
cfg.AuthScheme = ConfigDefault.AuthScheme
}
}
if cfg.FallbackKeyLookups == nil {
cfg.FallbackKeyLookups = []string{}
}
if cfg.Validator == nil {
panic("fiber: keyauth middleware requires a validator function")
}
Expand Down
59 changes: 48 additions & 11 deletions middleware/keyauth/keyauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keyauth

import (
"errors"
"fmt"
"net/url"
"strings"

Expand All @@ -28,23 +29,40 @@ 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])

var extractor extractorFunc
extractor, err := parseSingleExtractor(cfg.KeyLookup, cfg.AuthScheme)
if err != nil {
panic(fmt.Errorf("error creating middleware: invalid keyauth Config.KeyLookup: %w", err))
gaby marked this conversation as resolved.
Show resolved Hide resolved
}
if len(cfg.FallbackKeyLookups) > 0 {
subExtractors := map[string]extractorFunc{cfg.KeyLookup: extractor}
for _, keyLookup := range cfg.FallbackKeyLookups {
subExtractors[keyLookup], err = parseSingleExtractor(keyLookup, cfg.AuthScheme)
if err != nil {
panic(fmt.Errorf("error creating middleware: invalid keyauth Config.FallbackKeyLookups[%s]: %w", keyLookup, err))
}
}
extractor = func(c fiber.Ctx) (string, error) {
for keyLookup, subExtractor := range subExtractors {
res, err := subExtractor(c)
if err == nil && res != "" {
return res, nil
}
if !errors.Is(err, ErrMissingOrMalformedAPIKey) {
return "", fmt.Errorf("[%s] %w", keyLookup, err)
}
}
return "", ErrMissingOrMalformedAPIKey
}
dave-gray101 marked this conversation as resolved.
Show resolved Hide resolved
}

// Return middleware handler
Expand Down Expand Up @@ -80,6 +98,25 @@ func TokenFromContext(c fiber.Ctx) string {
return token
}

func parseSingleExtractor(keyLookup, authScheme string) (extractorFunc, error) {
parts := strings.Split(keyLookup, ":")
if len(parts) <= 1 {
return nil, fmt.Errorf("invalid keyLookup: %s", keyLookup)
dave-gray101 marked this conversation as resolved.
Show resolved Hide resolved
}
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])
case form:
extractor = keyFromForm(parts[1])
case param:
extractor = keyFromParam(parts[1])
case cookie:
extractor = keyFromCookie(parts[1])
}
return extractor, nil
}
dave-gray101 marked this conversation as resolved.
Show resolved Hide resolved

// keyFromHeader returns a function that extracts api key from the request header.
func keyFromHeader(header, authScheme string) func(c fiber.Ctx) (string, error) {
return func(c fiber.Ctx) (string, error) {
Expand Down
46 changes: 46 additions & 0 deletions middleware/keyauth/keyauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,52 @@ func Test_AuthSources(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",
FallbackKeyLookups: []string{"cookie:key", "query:key"},
Validator: func(_ 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)
require.NoError(t, err)
q := req.URL.Query()
q.Add("key", CorrectKey)
req.URL.RawQuery = q.Encode()

res, err := app.Test(req, -1)

require.NoError(t, err)

// test the body of the request
body, err := io.ReadAll(res.Body)
require.Equal(t, 200, res.StatusCode, desc)
// body
require.NoError(t, err)
require.Equal(t, success, string(body), desc)

err = res.Body.Close()
require.NoError(t, err)
}

func Test_MultipleKeyAuth(t *testing.T) {
// setup the fiber endpoint
app := fiber.New()
Expand Down