Skip to content

Commit 3c3ee31

Browse files
authored
Merge pull request #32 from go-pkgz/hashed-auth
Hashed auth
2 parents fbfa8cb + 80d630d commit 3c3ee31

File tree

10 files changed

+532
-23
lines changed

10 files changed

+532
-23
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ jobs:
1111
runs-on: ubuntu-latest
1212

1313
steps:
14-
- name: set up go 1.21
14+
- name: set up go 1.23
1515
uses: actions/setup-go@v3
1616
with:
17-
go-version: "1.21"
17+
go-version: "1.23"
1818
id: go
1919

2020
- name: checkout
@@ -33,11 +33,11 @@ jobs:
3333
- name: golangci-lint
3434
uses: golangci/golangci-lint-action@v3
3535
with:
36-
version: latest
36+
version: v1.61
3737

3838
- name: install goveralls
3939
run: |
40-
GO111MODULE=off go get -u -v github.com/mattn/goveralls
40+
go install github.com/mattn/goveralls@latest
4141
4242
- name: submit coverage
4343
run: $(go env GOPATH)/bin/goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov

.golangci.yml

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
run:
2+
timeout: 5m
3+
tests: false
4+
15
linters-settings:
26
govet:
37
enable:
48
- shadow
5-
gocyclo:
6-
min-complexity: 15
7-
dupl:
8-
threshold: 100
99
goconst:
1010
min-len: 2
1111
min-occurrences: 2
@@ -20,37 +20,47 @@ linters-settings:
2020
- experimental
2121
disabled-checks:
2222
- wrapperFunc
23+
- hugeParam
24+
- rangeValCopy
25+
- singleCaseSwitch
26+
- ifElseChain
2327

2428
linters:
2529
enable:
26-
- staticcheck
27-
- gosimple
2830
- revive
2931
- govet
3032
- unconvert
33+
- staticcheck
3134
- unused
3235
- gosec
33-
- gocyclo
3436
- dupl
3537
- misspell
3638
- unparam
3739
- typecheck
3840
- ineffassign
3941
- stylecheck
4042
- gochecknoinits
41-
- exportloopref
43+
- copyloopvar
4244
- gocritic
4345
- nakedret
46+
- gosimple
4447
- prealloc
4548
fast: false
4649
disable-all: true
4750

4851
issues:
52+
exclude-dirs:
53+
- vendor
4954
exclude-rules:
50-
- text: 'Deferring unsafe method "Close" on type "io.ReadCloser"'
55+
- text: "at least one file in a package should have a package comment"
5156
linters:
52-
- gosec
57+
- stylecheck
58+
- text: "should have a package comment"
59+
linters:
60+
- revive
5361
- path: _test\.go
5462
linters:
63+
- gosec
5564
- dupl
56-
exclude-use-default: false
65+
exclude-use-default: false
66+

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,58 @@ example with chi router:
182182
router.Use(rest.Reject(http.StatusBadRequest, "X-Request-Id header is required", rejectFn))
183183
```
184184

185+
### BasicAuth middleware family
186+
187+
The package provides several BasicAuth middleware implementations for different authentication needs:
188+
189+
#### BasicAuth
190+
The base middleware that requires basic auth and matches user & passwd with a client-provided checker function.
191+
```go
192+
checkFn := func(user, passwd string) bool {
193+
return user == "admin" && passwd == "secret"
194+
}
195+
router.Use(rest.BasicAuth(checkFn))
196+
```
197+
198+
#### BasicAuthWithUserPasswd
199+
A simpler version comparing user & password with provided values directly.
200+
```go
201+
router.Use(rest.BasicAuthWithUserPasswd("admin", "secret"))
202+
```
203+
204+
#### BasicAuthWithBcryptHash
205+
Matches username and bcrypt-hashed password. Useful when storing hashed passwords.
206+
```go
207+
hash, err := rest.GenerateBcryptHash("secret")
208+
if err != nil {
209+
// handle error
210+
}
211+
router.Use(rest.BasicAuthWithBcryptHash("admin", hash))
212+
```
213+
214+
#### BasicAuthWithArgon2Hash
215+
Similar to bcrypt version but uses Argon2id hash with a separate salt. Both hash and salt are base64 encoded.
216+
```go
217+
hash, salt, err := rest.GenerateArgon2Hash("secret")
218+
if err != nil {
219+
// handle error
220+
}
221+
router.Use(rest.BasicAuthWithArgon2Hash("admin", hash, salt))
222+
```
223+
224+
#### BasicAuthWithPrompt
225+
Similar to BasicAuthWithUserPasswd but adds browser's authentication prompt by setting the WWW-Authenticate header.
226+
```go
227+
router.Use(rest.BasicAuthWithPrompt("admin", "secret"))
228+
```
229+
230+
All BasicAuth middlewares:
231+
- Return `StatusUnauthorized` (401) if no auth header provided
232+
- Return `StatusForbidden` (403) if credentials check failed
233+
- Add IsAuthorized flag to the request context, retrievable with `rest.IsAuthorized(r.Context())`
234+
- Use constant-time comparison to prevent timing attacks
235+
- Support secure password hashing with bcrypt and Argon2id
236+
185237
### Benchmarks middleware
186238

187239
Benchmarks middleware allows measuring the time of request handling, number of requests per second and report aggregated metrics.

basic_auth.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ package rest
22

33
import (
44
"context"
5+
"crypto/rand"
56
"crypto/subtle"
7+
"encoding/base64"
68
"net/http"
9+
10+
"golang.org/x/crypto/argon2"
11+
"golang.org/x/crypto/bcrypt"
712
)
813

914
const baContextKey = "authorizedWithBasicAuth"
@@ -39,6 +44,42 @@ func BasicAuthWithUserPasswd(user, passwd string) func(http.Handler) http.Handle
3944
return BasicAuth(checkFn)
4045
}
4146

47+
// BasicAuthWithBcryptHash middleware requires basic auth and matches user & bcrypt hashed password
48+
func BasicAuthWithBcryptHash(user, hashedPassword string) func(http.Handler) http.Handler {
49+
checkFn := func(reqUser, reqPasswd string) bool {
50+
if reqUser != user {
51+
return false
52+
}
53+
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(reqPasswd))
54+
return err == nil
55+
}
56+
return BasicAuth(checkFn)
57+
}
58+
59+
// BasicAuthWithArgon2Hash middleware requires basic auth and matches user & argon2 hashed password
60+
// both hashedPassword and salt must be base64 encoded strings
61+
// Uses Argon2id with parameters: t=1, m=64*1024 KB, p=4 threads
62+
func BasicAuthWithArgon2Hash(user, hashedPassword, salt string) func(http.Handler) http.Handler {
63+
checkFn := func(reqUser, reqPasswd string) bool {
64+
if reqUser != user {
65+
return false
66+
}
67+
68+
saltBytes, err := base64.StdEncoding.DecodeString(salt)
69+
if err != nil {
70+
return false
71+
}
72+
storedHashBytes, err := base64.StdEncoding.DecodeString(hashedPassword)
73+
if err != nil {
74+
return false
75+
}
76+
77+
hash := argon2.IDKey([]byte(reqPasswd), saltBytes, 1, 64*1024, 4, 32)
78+
return subtle.ConstantTimeCompare(hash, storedHashBytes) == 1
79+
}
80+
return BasicAuth(checkFn)
81+
}
82+
4283
// IsAuthorized returns true is user authorized.
4384
// it can be used in handlers to check if BasicAuth middleware was applied
4485
func IsAuthorized(ctx context.Context) bool {
@@ -71,3 +112,25 @@ func BasicAuthWithPrompt(user, passwd string) func(http.Handler) http.Handler {
71112
return http.HandlerFunc(fn)
72113
}
73114
}
115+
116+
// GenerateBcryptHash generates a bcrypt hash from a password
117+
func GenerateBcryptHash(password string) (string, error) {
118+
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
119+
if err != nil {
120+
return "", err
121+
}
122+
return string(hash), nil
123+
}
124+
125+
// GenerateArgon2Hash generates an argon2 hash and salt from a password
126+
func GenerateArgon2Hash(password string) (hash, salt string, err error) {
127+
saltBytes := make([]byte, 16)
128+
if _, err := rand.Read(saltBytes); err != nil {
129+
return "", "", err
130+
}
131+
132+
// using recommended parameters: time=1, memory=64*1024, threads=4, keyLen=32
133+
hashBytes := argon2.IDKey([]byte(password), saltBytes, 1, 64*1024, 4, 32)
134+
135+
return base64.StdEncoding.EncodeToString(hashBytes), base64.StdEncoding.EncodeToString(saltBytes), nil
136+
}

0 commit comments

Comments
 (0)