Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion docs/middleware/basicauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ id: basicauth

Basic Authentication middleware for [Fiber](https://github.com/gofiber/fiber) that provides an HTTP basic authentication. It calls the next handler for valid credentials and [401 Unauthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) or a custom response for missing or invalid credentials.

The default unauthorized response includes the header `WWW-Authenticate: Basic realm="Restricted"`.
The default unauthorized response includes the header `WWW-Authenticate: Basic realm="Restricted", charset="UTF-8"` and sets `Cache-Control: no-store`.

## Signatures

Expand Down Expand Up @@ -78,6 +78,8 @@ func handler(c fiber.Ctx) error {
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
| Users | `map[string]string` | Users defines the allowed credentials. | `map[string]string{}` |
| Realm | `string` | Realm is a string to define the realm attribute of BasicAuth. The realm identifies the system to authenticate against and can be used by clients to save credentials. | `"Restricted"` |
| Charset | `string` | Charset sent in the `WWW-Authenticate` header, so clients know how credentials are encoded. | `"UTF-8"` |
| StorePassword | `bool` | Store the plaintext password in the context and retrieve it via `PasswordFromContext`. | `false` |
| Authorizer | `func(string, string) bool` | Authorizer defines a function to check the credentials. It will be called with a username and password and is expected to return true or false to indicate approval. | `nil` |
| Unauthorized | `fiber.Handler` | Unauthorized defines the response body for unauthorized responses. | `nil` |

Expand All @@ -88,6 +90,8 @@ var ConfigDefault = Config{
Next: nil,
Users: map[string]string{},
Realm: "Restricted",
Charset: "UTF-8",
StorePassword: false,
Authorizer: nil,
Unauthorized: nil,
}
Expand Down
2 changes: 1 addition & 1 deletion docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,7 @@ The adaptor middleware has been significantly optimized for performance and effi

### BasicAuth

The BasicAuth middleware was updated for improved robustness in parsing the Authorization header, with enhanced validation and whitespace handling. The default unauthorized response now uses a properly quoted and capitalized `WWW-Authenticate` header.
The BasicAuth middleware now validates the `Authorization` header more rigorously and sets security-focused response headers. The default challenge includes the `charset="UTF-8"` parameter and disables caching. Passwords are no longer stored in the request context by default; use the new `StorePassword` option to retain them. A `Charset` option controls the value used in the challenge header.

### Cache

Expand Down
14 changes: 10 additions & 4 deletions middleware/basicauth/basicauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const (
passwordKey
)

const basicScheme = "Basic"

// New creates a new middleware handler
func New(config Config) fiber.Handler {
// Set default config
Expand All @@ -30,12 +32,14 @@ func New(config Config) fiber.Handler {
return c.Next()
}

// Get authorization header
// Get authorization header and ensure it matches the Basic scheme
auth := utils.Trim(c.Get(fiber.HeaderAuthorization), ' ')
if auth == "" {
return cfg.Unauthorized(c)
Comment thread
gaby marked this conversation as resolved.
}

// Expect a scheme token followed by credentials
parts := strings.Fields(auth)
if len(parts) != 2 || !utils.EqualFold(parts[0], "basic") {
if len(parts) != 2 || !utils.EqualFold(parts[0], basicScheme) {
return cfg.Unauthorized(c)
}

Expand Down Expand Up @@ -66,7 +70,9 @@ func New(config Config) fiber.Handler {

if cfg.Authorizer(username, password) {
c.Locals(usernameKey, username)
c.Locals(passwordKey, password)
if cfg.StorePassword {
c.Locals(passwordKey, password)
}
return c.Next()
}

Expand Down
24 changes: 23 additions & 1 deletion middleware/basicauth/basicauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func Test_Middleware_BasicAuth(t *testing.T) {
"john": "doe",
"admin": "123456",
},
StorePassword: true,
}))

app.Get("/testauth", func(c fiber.Ctx) error {
Expand Down Expand Up @@ -91,6 +92,27 @@ func Test_Middleware_BasicAuth(t *testing.T) {
}
}

func Test_BasicAuth_NoStorePassword(t *testing.T) {
t.Parallel()
app := fiber.New()

app.Use(New(Config{
Users: map[string]string{"john": "doe"},
}))

app.Get("/", func(c fiber.Ctx) error {
require.Empty(t, PasswordFromContext(c))
return c.SendStatus(fiber.StatusOK)
})

creds := base64.StdEncoding.EncodeToString([]byte("john:doe"))
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
req.Header.Set(fiber.HeaderAuthorization, "Basic "+creds)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
}

func Test_BasicAuth_WWWAuthenticateHeader(t *testing.T) {
t.Parallel()
app := fiber.New()
Expand All @@ -100,7 +122,7 @@ func Test_BasicAuth_WWWAuthenticateHeader(t *testing.T) {
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)
require.Equal(t, `Basic realm="Restricted"`, resp.Header.Get(fiber.HeaderWWWAuthenticate))
require.Equal(t, `Basic realm="Restricted", charset="UTF-8"`, resp.Header.Get(fiber.HeaderWWWAuthenticate))
}

func Test_BasicAuth_InvalidHeader(t *testing.T) {
Expand Down
36 changes: 30 additions & 6 deletions middleware/basicauth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,30 @@ type Config struct {
//
// Optional. Default: "Restricted".
Realm string

// Charset defines the value for the charset parameter in the
// WWW-Authenticate header. According to RFC 7617 clients can use
// this value to interpret credentials correctly.
//
// Optional. Default: "UTF-8".
Charset string

// StorePassword determines if the plaintext password should be stored
// in the context for later retrieval via PasswordFromContext.
//
// Optional. Default: false.
StorePassword bool
}

// ConfigDefault is the default config
var ConfigDefault = Config{
Next: nil,
Users: map[string]string{},
Realm: "Restricted",
Authorizer: nil,
Unauthorized: nil,
Next: nil,
Users: map[string]string{},
Realm: "Restricted",
Charset: "UTF-8",
StorePassword: false,
Authorizer: nil,
Unauthorized: nil,
}

// Helper function to set default values
Expand All @@ -72,6 +87,9 @@ func configDefault(config ...Config) Config {
if cfg.Realm == "" {
cfg.Realm = ConfigDefault.Realm
}
if cfg.Charset == "" {
cfg.Charset = ConfigDefault.Charset
}
if cfg.Authorizer == nil {
cfg.Authorizer = func(user, pass string) bool {
userPwd, exist := cfg.Users[user]
Expand All @@ -80,7 +98,13 @@ func configDefault(config ...Config) Config {
}
if cfg.Unauthorized == nil {
cfg.Unauthorized = func(c fiber.Ctx) error {
c.Set(fiber.HeaderWWWAuthenticate, "Basic realm="+strconv.Quote(cfg.Realm))
header := "Basic realm=" + strconv.Quote(cfg.Realm)
if cfg.Charset != "" {
header += ", charset=" + strconv.Quote(cfg.Charset)
}
c.Set(fiber.HeaderWWWAuthenticate, header)
c.Set(fiber.HeaderCacheControl, "no-store")
c.Set(fiber.HeaderVary, fiber.HeaderAuthorization)
return c.SendStatus(fiber.StatusUnauthorized)
}
}
Expand Down