Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
51 changes: 43 additions & 8 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,15 +87,35 @@ 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]
return exist && subtle.ConstantTimeCompare(utils.UnsafeBytes(userPwd), utils.UnsafeBytes(pass)) == 1
var pwd string
var ok bool
for u, p := range cfg.Users {
match := subtle.ConstantTimeCompare(utils.UnsafeBytes(u), utils.UnsafeBytes(user)) == 1
if match {
pwd = p
ok = true
}
}
if !ok {
return false
}
return subtle.ConstantTimeCompare(utils.UnsafeBytes(pwd), utils.UnsafeBytes(pass)) == 1
Comment thread
gaby marked this conversation as resolved.
Outdated
}
}
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