Skip to content

Commit 6d16bf5

Browse files
authored
🧹chore: Improve BasicAuth middleware default security (#3522)
* Refine BasicAuth middleware * Fix lint issues * Update basicauth.md
1 parent aea0fd2 commit 6d16bf5

File tree

5 files changed

+69
-13
lines changed

5 files changed

+69
-13
lines changed

docs/middleware/basicauth.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ id: basicauth
66

77
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.
88

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

1111
## Signatures
1212

@@ -78,6 +78,8 @@ func handler(c fiber.Ctx) error {
7878
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
7979
| Users | `map[string]string` | Users defines the allowed credentials. | `map[string]string{}` |
8080
| 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"` |
81+
| Charset | `string` | Charset sent in the `WWW-Authenticate` header, so clients know how credentials are encoded. | `"UTF-8"` |
82+
| StorePassword | `bool` | Store the plaintext password in the context and retrieve it via `PasswordFromContext`. | `false` |
8183
| 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` |
8284
| Unauthorized | `fiber.Handler` | Unauthorized defines the response body for unauthorized responses. | `nil` |
8385

@@ -88,6 +90,8 @@ var ConfigDefault = Config{
8890
Next: nil,
8991
Users: map[string]string{},
9092
Realm: "Restricted",
93+
Charset: "UTF-8",
94+
StorePassword: false,
9195
Authorizer: nil,
9296
Unauthorized: nil,
9397
}

docs/whats_new.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -975,7 +975,7 @@ The adaptor middleware has been significantly optimized for performance and effi
975975

976976
### BasicAuth
977977

978-
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.
978+
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.
979979

980980
### Cache
981981

middleware/basicauth/basicauth.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const (
1818
passwordKey
1919
)
2020

21+
const basicScheme = "Basic"
22+
2123
// New creates a new middleware handler
2224
func New(config Config) fiber.Handler {
2325
// Set default config
@@ -30,12 +32,14 @@ func New(config Config) fiber.Handler {
3032
return c.Next()
3133
}
3234

33-
// Get authorization header
35+
// Get authorization header and ensure it matches the Basic scheme
3436
auth := utils.Trim(c.Get(fiber.HeaderAuthorization), ' ')
37+
if auth == "" {
38+
return cfg.Unauthorized(c)
39+
}
3540

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

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

6771
if cfg.Authorizer(username, password) {
6872
c.Locals(usernameKey, username)
69-
c.Locals(passwordKey, password)
73+
if cfg.StorePassword {
74+
c.Locals(passwordKey, password)
75+
}
7076
return c.Next()
7177
}
7278

middleware/basicauth/basicauth_test.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func Test_Middleware_BasicAuth(t *testing.T) {
3636
"john": "doe",
3737
"admin": "123456",
3838
},
39+
StorePassword: true,
3940
}))
4041

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

95+
func Test_BasicAuth_NoStorePassword(t *testing.T) {
96+
t.Parallel()
97+
app := fiber.New()
98+
99+
app.Use(New(Config{
100+
Users: map[string]string{"john": "doe"},
101+
}))
102+
103+
app.Get("/", func(c fiber.Ctx) error {
104+
require.Empty(t, PasswordFromContext(c))
105+
return c.SendStatus(fiber.StatusOK)
106+
})
107+
108+
creds := base64.StdEncoding.EncodeToString([]byte("john:doe"))
109+
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
110+
req.Header.Set(fiber.HeaderAuthorization, "Basic "+creds)
111+
resp, err := app.Test(req)
112+
require.NoError(t, err)
113+
require.Equal(t, fiber.StatusOK, resp.StatusCode)
114+
}
115+
94116
func Test_BasicAuth_WWWAuthenticateHeader(t *testing.T) {
95117
t.Parallel()
96118
app := fiber.New()
@@ -100,7 +122,7 @@ func Test_BasicAuth_WWWAuthenticateHeader(t *testing.T) {
100122
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
101123
require.NoError(t, err)
102124
require.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)
103-
require.Equal(t, `Basic realm="Restricted"`, resp.Header.Get(fiber.HeaderWWWAuthenticate))
125+
require.Equal(t, `Basic realm="Restricted", charset="UTF-8"`, resp.Header.Get(fiber.HeaderWWWAuthenticate))
104126
}
105127

106128
func Test_BasicAuth_InvalidHeader(t *testing.T) {

middleware/basicauth/config.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,30 @@ type Config struct {
4141
//
4242
// Optional. Default: "Restricted".
4343
Realm string
44+
45+
// Charset defines the value for the charset parameter in the
46+
// WWW-Authenticate header. According to RFC 7617 clients can use
47+
// this value to interpret credentials correctly.
48+
//
49+
// Optional. Default: "UTF-8".
50+
Charset string
51+
52+
// StorePassword determines if the plaintext password should be stored
53+
// in the context for later retrieval via PasswordFromContext.
54+
//
55+
// Optional. Default: false.
56+
StorePassword bool
4457
}
4558

4659
// ConfigDefault is the default config
4760
var ConfigDefault = Config{
48-
Next: nil,
49-
Users: map[string]string{},
50-
Realm: "Restricted",
51-
Authorizer: nil,
52-
Unauthorized: nil,
61+
Next: nil,
62+
Users: map[string]string{},
63+
Realm: "Restricted",
64+
Charset: "UTF-8",
65+
StorePassword: false,
66+
Authorizer: nil,
67+
Unauthorized: nil,
5368
}
5469

5570
// Helper function to set default values
@@ -72,6 +87,9 @@ func configDefault(config ...Config) Config {
7287
if cfg.Realm == "" {
7388
cfg.Realm = ConfigDefault.Realm
7489
}
90+
if cfg.Charset == "" {
91+
cfg.Charset = ConfigDefault.Charset
92+
}
7593
if cfg.Authorizer == nil {
7694
cfg.Authorizer = func(user, pass string) bool {
7795
userPwd, exist := cfg.Users[user]
@@ -80,7 +98,13 @@ func configDefault(config ...Config) Config {
8098
}
8199
if cfg.Unauthorized == nil {
82100
cfg.Unauthorized = func(c fiber.Ctx) error {
83-
c.Set(fiber.HeaderWWWAuthenticate, "Basic realm="+strconv.Quote(cfg.Realm))
101+
header := "Basic realm=" + strconv.Quote(cfg.Realm)
102+
if cfg.Charset != "" {
103+
header += ", charset=" + strconv.Quote(cfg.Charset)
104+
}
105+
c.Set(fiber.HeaderWWWAuthenticate, header)
106+
c.Set(fiber.HeaderCacheControl, "no-store")
107+
c.Set(fiber.HeaderVary, fiber.HeaderAuthorization)
84108
return c.SendStatus(fiber.StatusUnauthorized)
85109
}
86110
}

0 commit comments

Comments
 (0)