Skip to content
15 changes: 6 additions & 9 deletions docs/middleware/keyauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,11 @@ 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` |
| ErrorHandler | `fiber.ErrorHandler` | ErrorHandler defines a function which is executed for an invalid key. By default a 401 response with a `WWW-Authenticate` challenge is sent. | `nil` |
| KeyLookup | `string` | KeyLookup is a string in the form of "`<source>:<name>`" that is used to extract the key from the request. | "header:Authorization" |
| CustomKeyLookup | `KeyLookupFunc` aka `func(c fiber.Ctx) (string, error)` | If more complex logic is required to extract the key from the request, an arbitrary function to extract it can be specified here. Utility helper functions are described below. | `nil` |
| AuthScheme | `string` | AuthScheme to be used in the Authorization header. | "Bearer" |
| Realm | `string` | Realm specifies the protected area name used in the `WWW-Authenticate` header. | `"Restricted"` |
| Validator | `func(fiber.Ctx, string) (bool, error)` | Validator is a function to validate the key. | A function for key validation |

## Default Config
Expand All @@ -230,15 +231,11 @@ var ConfigDefault = Config{
SuccessHandler: func(c fiber.Ctx) error {
return c.Next()
},
ErrorHandler: func(c fiber.Ctx, err error) error {
if err == ErrMissingOrMalformedAPIKey {
return c.Status(fiber.StatusUnauthorized).SendString(err.Error())
}
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired API Key")
},
KeyLookup: "header:" + fiber.HeaderAuthorization,
ErrorHandler: nil,
KeyLookup: "header:" + fiber.HeaderAuthorization,
CustomKeyLookup: nil,
AuthScheme: "Bearer",
AuthScheme: "Bearer",
Realm: "Restricted",
}
```

Expand Down
38 changes: 25 additions & 13 deletions middleware/keyauth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keyauth

import (
"errors"
"fmt"

"github.com/gofiber/fiber/v3"
)
Expand Down Expand Up @@ -42,22 +43,22 @@ type Config struct {
// AuthScheme to be used in the Authorization header.
// Optional. Default value "Bearer".
AuthScheme string

// Realm defines the protected area for WWW-Authenticate responses.
// Optional. Default value "Restricted".
Realm string
}

// ConfigDefault is the default config
var ConfigDefault = Config{
SuccessHandler: func(c fiber.Ctx) error {
return c.Next()
},
ErrorHandler: func(c fiber.Ctx, err error) error {
if errors.Is(err, ErrMissingOrMalformedAPIKey) {
return c.Status(fiber.StatusUnauthorized).SendString(err.Error())
}
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired API Key")
},
ErrorHandler: nil,
KeyLookup: "header:" + fiber.HeaderAuthorization,
CustomKeyLookup: nil,
AuthScheme: "Bearer",
Realm: "Restricted",
}

// Helper function to set default values
Expand All @@ -71,19 +72,30 @@ func configDefault(config ...Config) Config {
cfg := config[0]

// Set default values
if cfg.SuccessHandler == nil {
cfg.SuccessHandler = ConfigDefault.SuccessHandler
}
if cfg.ErrorHandler == nil {
cfg.ErrorHandler = ConfigDefault.ErrorHandler
}
if cfg.KeyLookup == "" {
cfg.KeyLookup = ConfigDefault.KeyLookup
// set AuthScheme as "Bearer" only if KeyLookup is set to default.
if cfg.AuthScheme == "" {
cfg.AuthScheme = ConfigDefault.AuthScheme
}
}
if cfg.Realm == "" {
cfg.Realm = ConfigDefault.Realm
}
if cfg.SuccessHandler == nil {
cfg.SuccessHandler = ConfigDefault.SuccessHandler
}
if cfg.ErrorHandler == nil {
localCfg := cfg
cfg.ErrorHandler = func(c fiber.Ctx, err error) error {
if localCfg.AuthScheme != "" {
c.Set(fiber.HeaderWWWAuthenticate, fmt.Sprintf("%s realm=%q", localCfg.AuthScheme, localCfg.Realm))
}
if errors.Is(err, ErrMissingOrMalformedAPIKey) {
return c.Status(fiber.StatusUnauthorized).SendString(err.Error())
}
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired API Key")
}
}
Comment thread
gaby marked this conversation as resolved.
if cfg.Validator == nil {
panic("fiber: keyauth middleware requires a validator function")
}
Expand Down
21 changes: 15 additions & 6 deletions middleware/keyauth/keyauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,24 @@ func DefaultKeyLookup(keyLookup, authScheme string) (KeyLookupFunc, error) {
// keyFromHeader returns a function that extracts api key from the request header.
func KeyFromHeader(header, authScheme string) KeyLookupFunc {
return func(c fiber.Ctx) (string, error) {
auth := c.Get(header)
l := len(authScheme)
if len(auth) > 0 && l == 0 {
auth := strings.TrimSpace(c.Get(header))
Comment thread
gaby marked this conversation as resolved.
Outdated
if auth == "" {
return "", ErrMissingOrMalformedAPIKey
}

if authScheme == "" {
return auth, nil
}
if len(auth) > l+1 && auth[:l] == authScheme {
return auth[l+1:], nil

l := len(authScheme)
if len(auth) <= l || !strings.EqualFold(auth[:l], authScheme) {
Comment thread
gaby marked this conversation as resolved.
Outdated
return "", ErrMissingOrMalformedAPIKey
}
return "", ErrMissingOrMalformedAPIKey
if len(auth) <= l+1 || auth[l] != ' ' {
return "", ErrMissingOrMalformedAPIKey
}

return strings.TrimSpace(auth[l+1:]), nil
Comment thread
gaby marked this conversation as resolved.
Outdated
}
Comment thread
gaby marked this conversation as resolved.
Outdated
Comment thread
gaby marked this conversation as resolved.
}

Expand Down
57 changes: 57 additions & 0 deletions middleware/keyauth/keyauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keyauth

import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -614,3 +615,59 @@ func Test_AuthSchemeBasic(t *testing.T) {
require.Equal(t, http.StatusOK, res.StatusCode)
require.Equal(t, "API key is valid", string(body))
}

func Test_HeaderSchemeCaseInsensitive(t *testing.T) {
app := fiber.New()
app.Use(New(Config{
Validator: func(_ fiber.Ctx, key string) (bool, error) {
if key == CorrectKey {
return true, nil
}
return false, ErrMissingOrMalformedAPIKey
},
}))
app.Get("/", func(c fiber.Ctx) error { return c.SendString("OK") })

req := httptest.NewRequest(fiber.MethodGet, "/", nil)
req.Header.Add("Authorization", "bearer "+CorrectKey)
res, err := app.Test(req)
require.NoError(t, err)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, http.StatusOK, res.StatusCode)
require.Equal(t, "OK", string(body))
}

func Test_DefaultErrorHandlerChallenge(t *testing.T) {
app := fiber.New()
app.Use(New(Config{
Validator: func(_ fiber.Ctx, _ string) (bool, error) {
return false, ErrMissingOrMalformedAPIKey
},
}))
app.Get("/", func(c fiber.Ctx) error { return c.SendString("OK") })

res, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
require.Equal(t, "Bearer realm=\"Restricted\"", res.Header.Get("WWW-Authenticate"))
}

func Test_DefaultErrorHandlerGenericError(t *testing.T) {
app := fiber.New()
app.Use(New(Config{
AuthScheme: "Bearer",
Validator: func(_ fiber.Ctx, _ string) (bool, error) {
return false, errors.New("token expired")
},
}))
app.Get("/", func(c fiber.Ctx) error { return c.SendString("OK") })

req := httptest.NewRequest(fiber.MethodGet, "/", nil)
res, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
body, _ := io.ReadAll(res.Body)
require.Equal(t, "Invalid or expired API Key", string(body))
require.Equal(t, `Bearer realm="Restricted"`, res.Header.Get("WWW-Authenticate"))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.