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
35 changes: 23 additions & 12 deletions docs/middleware/basicauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,25 @@ After you initiate your Fiber app, you can use the following possibilities:
// Provide a minimal config
app.Use(basicauth.New(basicauth.Config{
Users: map[string]string{
"john": "doe",
"admin": "123456",
// "doe" hashed using SHA-256
"john": "{SHA256}eZ75KhGvkY4/t0HfQpNPO1aO0tk6wd908bjUGieTKm8=",
// "123456" hashed using bcrypt
"admin": "$2a$10$gTYwCN66/tBRoCr3.TXa1.v1iyvwIF7GRBqxzv7G.AHLMt/owXrp.",
},
}))

// Or extend your config for customization
app.Use(basicauth.New(basicauth.Config{
Users: map[string]string{
"john": "doe",
"admin": "123456",
// "doe" hashed using SHA-256
"john": "{SHA256}eZ75KhGvkY4/t0HfQpNPO1aO0tk6wd908bjUGieTKm8=",
// "123456" hashed using bcrypt
"admin": "$2a$10$gTYwCN66/tBRoCr3.TXa1.v1iyvwIF7GRBqxzv7G.AHLMt/owXrp.",
},
Realm: "Forbidden",
Authorizer: func(user, pass string, c fiber.Ctx) bool {
if user == "john" && pass == "doe" {
return true
}
if user == "admin" && pass == "123456" {
return true
}
return false
// custom validation logic
return (user == "john" || user == "admin")
},
Comment on lines 52 to 55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The example for Authorizer is a bit confusing as it doesn't use the pass parameter, which would result in a linter warning in a real Go program (pass declared and not used).

To make the example clearer and more practical, I suggest modifying it to demonstrate a simple custom validation that uses both the username and password.

Suggested change
Authorizer: func(user, pass string, c fiber.Ctx) bool {
if user == "john" && pass == "doe" {
return true
}
if user == "admin" && pass == "123456" {
return true
}
return false
// custom validation logic
return (user == "john" || user == "admin")
},
Authorizer: func(user, pass string, c fiber.Ctx) bool {
// custom validation logic
if user == "admin" && pass == "supersecret" {
return true
}
return false
},

Unauthorized: func(c fiber.Ctx) error {
return c.SendFile("./unauthorized.html")
Expand All @@ -62,6 +61,18 @@ app.Use(basicauth.New(basicauth.Config{

Getting the username and password

### Password hashes

Passwords must be supplied in pre-hashed form. The middleware detects the
hashing algorithm from a prefix:

- `"{SHA512}"`, `"{SHA256}"`, or `"{SHA}"` followed by a base64 encoded digest
- `"{MD5}"` followed by a base64 encoded digest
- standard bcrypt strings beginning with `$2`
Comment on lines +69 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add security warnings for weak hash algorithms

The documentation mentions {SHA} (SHA-1) and {MD5} without any security warnings. Please add a clear warning that these algorithms are insecure and supported only for backward compatibility. Recommend using bcrypt or SHA-256/SHA-512 instead.

- `"{SHA512}"`, `"{SHA256}"`, or `"{SHA}"` followed by a base64 encoded digest
- `"{MD5}"` followed by a base64 encoded digest
+ `"{SHA512}"`, `"{SHA256}"` followed by a base64 encoded digest
+ `"{SHA}"` (SHA-1) followed by a base64 encoded digest (**insecure, use only for backward compatibility**)
+ `"{MD5}"` followed by a base64 encoded digest (**insecure, use only for backward compatibility**)
- standard bcrypt strings beginning with `$2`
+ standard bcrypt strings beginning with `$2` (**recommended**)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `"{SHA512}"`, `"{SHA256}"`, or `"{SHA}"` followed by a base64 encoded digest
- `"{MD5}"` followed by a base64 encoded digest
- standard bcrypt strings beginning with `$2`
- `"{SHA512}"`, `"{SHA256}"` followed by a base64 encoded digest
- `"{SHA}"` (SHA-1) followed by a base64 encoded digest (**insecure, use only for backward compatibility**)
- `"{MD5}"` followed by a base64 encoded digest (**insecure, use only for backward compatibility**)
- standard bcrypt strings beginning with `$2` (**recommended**)
🤖 Prompt for AI Agents
In docs/middleware/basicauth.md around lines 69 to 71, add a clear security
warning that the `{SHA}` (SHA-1) and `{MD5}` hash algorithms are insecure and
only supported for backward compatibility. Update the documentation to recommend
using stronger algorithms like bcrypt or SHA-256/SHA-512 instead, emphasizing
best security practices.


If no prefix is present the value is interpreted as a SHA-256 digest encoded in
hex or base64. Plaintext passwords are rejected.

```go
func handler(c fiber.Ctx) error {
username := basicauth.UsernameFromContext(c)
Expand All @@ -76,7 +87,7 @@ func handler(c fiber.Ctx) error {
| Property | Type | Description | Default |
|:----------------|:----------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------|
| 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{}` |
| Users | `map[string]string` | Users maps usernames to **hashed** passwords (e.g. bcrypt, `{SHA256}`). | `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"` |
| HeaderLimit | `int` | Maximum allowed length of the `Authorization` header. Requests exceeding this limit are rejected. | `8192` |
Expand Down
4 changes: 3 additions & 1 deletion docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,7 @@ The adaptor middleware has been significantly optimized for performance and effi

### BasicAuth

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.
The BasicAuth middleware now validates the `Authorization` header more rigorously and sets security-focused response headers. Passwords must be provided in **hashed** form (e.g. SHA-256 or bcrypt) rather than plaintext. 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.
A new `HeaderLimit` option restricts the maximum length of the `Authorization` header (default: `8192` bytes).
The `Authorizer` function now receives the current `fiber.Ctx` as a third argument, allowing credential checks to incorporate request context.

Expand Down Expand Up @@ -1947,6 +1947,8 @@ Authorizer: func(user, pass string, _ fiber.Ctx) bool {
}
```

Passwords configured for BasicAuth must now be pre-hashed. If no prefix is supplied the middleware expects a SHA-256 digest encoded in hex. Common prefixes like `{SHA256}`, `{SHA}`, `{SHA512}`, `{MD5}` and bcrypt strings are also supported. Plaintext passwords are no longer accepted.

You can also set the optional `HeaderLimit`, `StorePassword`, and `Charset`
options to further control authentication behavior.

Expand Down
162 changes: 150 additions & 12 deletions middleware/basicauth/basicauth_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package basicauth

import (
"crypto/md5" // #nosec G501 - test compatibility
"crypto/sha1" // #nosec G505 - test compatibility
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http/httptest"
Expand All @@ -10,8 +15,29 @@ import (
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
"golang.org/x/crypto/bcrypt"
)

func sha256Hash(p string) string {
sum := sha256.Sum256([]byte(p))
return "{SHA256}" + base64.StdEncoding.EncodeToString(sum[:])
}

func sha512Hash(p string) string {
sum := sha512.Sum512([]byte(p))
return "{SHA512}" + base64.StdEncoding.EncodeToString(sum[:])
}

func sha1Hash(p string) string {
sum := sha1.Sum([]byte(p)) // #nosec G401 - test compatibility
return "{SHA}" + base64.StdEncoding.EncodeToString(sum[:])
}

func md5Hash(p string) string {
sum := md5.Sum([]byte(p)) // #nosec G401 - test compatibility
return "{MD5}" + base64.StdEncoding.EncodeToString(sum[:])
}

// go test -run Test_BasicAuth_Next
func Test_BasicAuth_Next(t *testing.T) {
t.Parallel()
Expand All @@ -31,10 +57,14 @@ func Test_Middleware_BasicAuth(t *testing.T) {
t.Parallel()
app := fiber.New()

hashedJohn := sha256Hash("doe")
hashedAdmin, err := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.MinCost)
require.NoError(t, err)

app.Use(New(Config{
Users: map[string]string{
"john": "doe",
"admin": "123456",
"john": hashedJohn,
"admin": string(hashedAdmin),
},
StorePassword: true,
}))
Expand Down Expand Up @@ -96,8 +126,10 @@ func Test_BasicAuth_NoStorePassword(t *testing.T) {
t.Parallel()
app := fiber.New()

hashedJohn := sha256Hash("doe")

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

app.Get("/", func(c fiber.Ctx) error {
Expand Down Expand Up @@ -143,7 +175,8 @@ func Test_BasicAuth_WWWAuthenticateHeader(t *testing.T) {
t.Parallel()
app := fiber.New()

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

resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
Expand All @@ -155,7 +188,8 @@ func Test_BasicAuth_InvalidHeader(t *testing.T) {
t.Parallel()
app := fiber.New()

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

req := httptest.NewRequest(fiber.MethodGet, "/", nil)
req.Header.Set(fiber.HeaderAuthorization, "Basic notbase64")
Expand All @@ -169,7 +203,8 @@ func Test_BasicAuth_EmptyAuthorization(t *testing.T) {
t.Parallel()
app := fiber.New()

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

cases := []string{"", " "}
for _, h := range cases {
Expand All @@ -185,7 +220,8 @@ func Test_BasicAuth_WhitespaceHandling(t *testing.T) {
t.Parallel()
app := fiber.New()

app.Use(New(Config{Users: map[string]string{"john": "doe"}}))
hashedJohn := sha256Hash("doe")
app.Use(New(Config{Users: map[string]string{"john": hashedJohn}}))
app.Get("/", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })

creds := base64.StdEncoding.EncodeToString([]byte("john:doe"))
Expand All @@ -209,11 +245,12 @@ func Test_BasicAuth_WhitespaceHandling(t *testing.T) {
func Test_BasicAuth_HeaderLimit(t *testing.T) {
t.Parallel()
creds := base64.StdEncoding.EncodeToString([]byte("john:doe"))
hashedJohn := sha256Hash("doe")

t.Run("too large", func(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{Users: map[string]string{"john": "doe"}, HeaderLimit: 10}))
app.Use(New(Config{Users: map[string]string{"john": hashedJohn}, HeaderLimit: 10}))
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
req.Header.Set(fiber.HeaderAuthorization, "Basic "+creds)
resp, err := app.Test(req)
Expand All @@ -224,7 +261,7 @@ func Test_BasicAuth_HeaderLimit(t *testing.T) {
t.Run("allowed", func(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{Users: map[string]string{"john": "doe"}, HeaderLimit: 100}))
app.Use(New(Config{Users: map[string]string{"john": hashedJohn}, HeaderLimit: 100}))
app.Get("/", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
req.Header.Set(fiber.HeaderAuthorization, "Basic "+creds)
Expand All @@ -238,9 +275,11 @@ func Test_BasicAuth_HeaderLimit(t *testing.T) {
func Benchmark_Middleware_BasicAuth(b *testing.B) {
app := fiber.New()

hashedJohn := sha256Hash("doe")

app.Use(New(Config{
Users: map[string]string{
"john": "doe",
"john": hashedJohn,
},
}))
app.Get("/", func(c fiber.Ctx) error {
Expand All @@ -267,9 +306,11 @@ func Benchmark_Middleware_BasicAuth(b *testing.B) {
func Benchmark_Middleware_BasicAuth_Upper(b *testing.B) {
app := fiber.New()

hashedJohn := sha256Hash("doe")

app.Use(New(Config{
Users: map[string]string{
"john": "doe",
"john": hashedJohn,
},
}))
app.Get("/", func(c fiber.Ctx) error {
Expand All @@ -296,7 +337,8 @@ func Test_BasicAuth_Immutable(t *testing.T) {
t.Parallel()
app := fiber.New(fiber.Config{Immutable: true})

app.Use(New(Config{Users: map[string]string{"john": "doe"}}))
hashedJohn := sha256Hash("doe")
app.Use(New(Config{Users: map[string]string{"john": hashedJohn}}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendStatus(fiber.StatusTeapot)
})
Expand All @@ -309,3 +351,99 @@ func Test_BasicAuth_Immutable(t *testing.T) {
require.NoError(t, err)
require.Equal(t, fiber.StatusTeapot, resp.StatusCode)
}

func Test_parseHashedPassword(t *testing.T) {
t.Parallel()
pass := "secret"
sha := sha256.Sum256([]byte(pass))
b64 := base64.StdEncoding.EncodeToString(sha[:])
hexDigest := hex.EncodeToString(sha[:])
bcryptHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.MinCost)
require.NoError(t, err)

cases := []struct {
name string
hashed string
}{
{"bcrypt", string(bcryptHash)},
{"sha512", sha512Hash(pass)},
{"sha256", sha256Hash(pass)},
{"sha256-hex", hexDigest},
{"sha256-b64", b64},
{"sha1", sha1Hash(pass)},
{"md5", md5Hash(pass)},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
verify, err := parseHashedPassword(tt.hashed)
require.NoError(t, err)
require.True(t, verify(pass))
require.False(t, verify("wrong"))
})
}
}

func Test_BasicAuth_HashVariants(t *testing.T) {
t.Parallel()
pass := "doe"
bcryptHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.MinCost)
require.NoError(t, err)
cases := []struct {
name string
hashed string
}{
{"bcrypt", string(bcryptHash)},
{"sha512", sha512Hash(pass)},
{"sha256", sha256Hash(pass)},
{"sha256-hex", func() string { h := sha256.Sum256([]byte(pass)); return hex.EncodeToString(h[:]) }()},
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The inline function makes the test case definition hard to read. Consider extracting this to a helper function like the other hash functions or computing it before the test cases array.

Suggested change
{"sha256-hex", func() string { h := sha256.Sum256([]byte(pass)); return hex.EncodeToString(h[:]) }()},
{"sha256-hex", sha256HexHash(pass)},

Copilot uses AI. Check for mistakes.
{"sha1", sha1Hash(pass)},
{"md5", md5Hash(pass)},
}

for _, tt := range cases {
app := fiber.New()
app.Use(New(Config{Users: map[string]string{"john": tt.hashed}}))
app.Get("/", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })

creds := base64.StdEncoding.EncodeToString([]byte("john:" + pass))
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.StatusTeapot, resp.StatusCode)
}
}

func Test_BasicAuth_HashVariants_Invalid(t *testing.T) {
t.Parallel()
pass := "doe"
wrong := "wrong"
bcryptHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.MinCost)
require.NoError(t, err)
cases := []struct {
name string
hashed string
}{
{"bcrypt", string(bcryptHash)},
{"sha512", sha512Hash(pass)},
{"sha256", sha256Hash(pass)},
{"sha256-hex", func() string { h := sha256.Sum256([]byte(pass)); return hex.EncodeToString(h[:]) }()},
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Duplicate inline function logic from the previous test. Consider creating a reusable helper function or variable to avoid code duplication.

Suggested change
{"sha256-hex", func() string { h := sha256.Sum256([]byte(pass)); return hex.EncodeToString(h[:]) }()},
{"sha256-hex", sha256HexHash(pass)},

Copilot uses AI. Check for mistakes.
{"sha1", sha1Hash(pass)},
{"md5", md5Hash(pass)},
}

for _, tt := range cases {
app := fiber.New()
app.Use(New(Config{Users: map[string]string{"john": tt.hashed}}))
app.Get("/", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })

creds := base64.StdEncoding.EncodeToString([]byte("john:" + wrong))
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.StatusUnauthorized, resp.StatusCode)
}
}
Loading
Loading