-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
🔥 feat: Support hashed BasicAuth passwords #3631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e280bdf
12a0299
facfa97
b5973aa
3da7dfd
c11533b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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") | ||||||||||||||||
| }, | ||||||||||||||||
| Unauthorized: func(c fiber.Ctx) error { | ||||||||||||||||
| return c.SendFile("./unauthorized.html") | ||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add security warnings for weak hash algorithms The documentation mentions - `"{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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| 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) | ||||||||||||||||
|
|
@@ -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` | | ||||||||||||||||
|
|
||||||||||||||||
| 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" | ||||||
|
|
@@ -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() | ||||||
|
|
@@ -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, | ||||||
| })) | ||||||
|
|
@@ -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 { | ||||||
|
|
@@ -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) | ||||||
|
|
@@ -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") | ||||||
|
|
@@ -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 { | ||||||
|
|
@@ -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")) | ||||||
|
|
@@ -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) | ||||||
|
|
@@ -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) | ||||||
|
|
@@ -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 { | ||||||
|
|
@@ -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 { | ||||||
|
|
@@ -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) | ||||||
| }) | ||||||
|
|
@@ -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[:]) }()}, | ||||||
|
||||||
| {"sha256-hex", func() string { h := sha256.Sum256([]byte(pass)); return hex.EncodeToString(h[:]) }()}, | |
| {"sha256-hex", sha256HexHash(pass)}, |
Copilot
AI
Jul 30, 2025
There was a problem hiding this comment.
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.
| {"sha256-hex", func() string { h := sha256.Sum256([]byte(pass)); return hex.EncodeToString(h[:]) }()}, | |
| {"sha256-hex", sha256HexHash(pass)}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example for
Authorizeris a bit confusing as it doesn't use thepassparameter, 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.