Skip to content

Commit

Permalink
Merge pull request #32 from go-pkgz/hashed-auth
Browse files Browse the repository at this point in the history
Hashed auth
  • Loading branch information
umputun authored Dec 9, 2024
2 parents fbfa8cb + 80d630d commit 3c3ee31
Show file tree
Hide file tree
Showing 10 changed files with 532 additions and 23 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: set up go 1.21
- name: set up go 1.23
uses: actions/setup-go@v3
with:
go-version: "1.21"
go-version: "1.23"
id: go

- name: checkout
Expand All @@ -33,11 +33,11 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
version: v1.61

- name: install goveralls
run: |
GO111MODULE=off go get -u -v github.com/mattn/goveralls
go install github.com/mattn/goveralls@latest
- name: submit coverage
run: $(go env GOPATH)/bin/goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov
Expand Down
32 changes: 21 additions & 11 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
run:
timeout: 5m
tests: false

linters-settings:
govet:
enable:
- shadow
gocyclo:
min-complexity: 15
dupl:
threshold: 100
goconst:
min-len: 2
min-occurrences: 2
Expand All @@ -20,37 +20,47 @@ linters-settings:
- experimental
disabled-checks:
- wrapperFunc
- hugeParam
- rangeValCopy
- singleCaseSwitch
- ifElseChain

linters:
enable:
- staticcheck
- gosimple
- revive
- govet
- unconvert
- staticcheck
- unused
- gosec
- gocyclo
- dupl
- misspell
- unparam
- typecheck
- ineffassign
- stylecheck
- gochecknoinits
- exportloopref
- copyloopvar
- gocritic
- nakedret
- gosimple
- prealloc
fast: false
disable-all: true

issues:
exclude-dirs:
- vendor
exclude-rules:
- text: 'Deferring unsafe method "Close" on type "io.ReadCloser"'
- text: "at least one file in a package should have a package comment"
linters:
- gosec
- stylecheck
- text: "should have a package comment"
linters:
- revive
- path: _test\.go
linters:
- gosec
- dupl
exclude-use-default: false
exclude-use-default: false

52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,58 @@ example with chi router:
router.Use(rest.Reject(http.StatusBadRequest, "X-Request-Id header is required", rejectFn))
```

### BasicAuth middleware family

The package provides several BasicAuth middleware implementations for different authentication needs:

#### BasicAuth
The base middleware that requires basic auth and matches user & passwd with a client-provided checker function.
```go
checkFn := func(user, passwd string) bool {
return user == "admin" && passwd == "secret"
}
router.Use(rest.BasicAuth(checkFn))
```

#### BasicAuthWithUserPasswd
A simpler version comparing user & password with provided values directly.
```go
router.Use(rest.BasicAuthWithUserPasswd("admin", "secret"))
```

#### BasicAuthWithBcryptHash
Matches username and bcrypt-hashed password. Useful when storing hashed passwords.
```go
hash, err := rest.GenerateBcryptHash("secret")
if err != nil {
// handle error
}
router.Use(rest.BasicAuthWithBcryptHash("admin", hash))
```

#### BasicAuthWithArgon2Hash
Similar to bcrypt version but uses Argon2id hash with a separate salt. Both hash and salt are base64 encoded.
```go
hash, salt, err := rest.GenerateArgon2Hash("secret")
if err != nil {
// handle error
}
router.Use(rest.BasicAuthWithArgon2Hash("admin", hash, salt))
```

#### BasicAuthWithPrompt
Similar to BasicAuthWithUserPasswd but adds browser's authentication prompt by setting the WWW-Authenticate header.
```go
router.Use(rest.BasicAuthWithPrompt("admin", "secret"))
```

All BasicAuth middlewares:
- Return `StatusUnauthorized` (401) if no auth header provided
- Return `StatusForbidden` (403) if credentials check failed
- Add IsAuthorized flag to the request context, retrievable with `rest.IsAuthorized(r.Context())`
- Use constant-time comparison to prevent timing attacks
- Support secure password hashing with bcrypt and Argon2id

### Benchmarks middleware

Benchmarks middleware allows measuring the time of request handling, number of requests per second and report aggregated metrics.
Expand Down
63 changes: 63 additions & 0 deletions basic_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ package rest

import (
"context"
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"net/http"

"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
)

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

// BasicAuthWithBcryptHash middleware requires basic auth and matches user & bcrypt hashed password
func BasicAuthWithBcryptHash(user, hashedPassword string) func(http.Handler) http.Handler {
checkFn := func(reqUser, reqPasswd string) bool {
if reqUser != user {
return false
}
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(reqPasswd))
return err == nil
}
return BasicAuth(checkFn)
}

// BasicAuthWithArgon2Hash middleware requires basic auth and matches user & argon2 hashed password
// both hashedPassword and salt must be base64 encoded strings
// Uses Argon2id with parameters: t=1, m=64*1024 KB, p=4 threads
func BasicAuthWithArgon2Hash(user, hashedPassword, salt string) func(http.Handler) http.Handler {
checkFn := func(reqUser, reqPasswd string) bool {
if reqUser != user {
return false
}

saltBytes, err := base64.StdEncoding.DecodeString(salt)
if err != nil {
return false
}
storedHashBytes, err := base64.StdEncoding.DecodeString(hashedPassword)
if err != nil {
return false
}

hash := argon2.IDKey([]byte(reqPasswd), saltBytes, 1, 64*1024, 4, 32)
return subtle.ConstantTimeCompare(hash, storedHashBytes) == 1
}
return BasicAuth(checkFn)
}

// IsAuthorized returns true is user authorized.
// it can be used in handlers to check if BasicAuth middleware was applied
func IsAuthorized(ctx context.Context) bool {
Expand Down Expand Up @@ -71,3 +112,25 @@ func BasicAuthWithPrompt(user, passwd string) func(http.Handler) http.Handler {
return http.HandlerFunc(fn)
}
}

// GenerateBcryptHash generates a bcrypt hash from a password
func GenerateBcryptHash(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}

// GenerateArgon2Hash generates an argon2 hash and salt from a password
func GenerateArgon2Hash(password string) (hash, salt string, err error) {
saltBytes := make([]byte, 16)
if _, err := rand.Read(saltBytes); err != nil {
return "", "", err
}

// using recommended parameters: time=1, memory=64*1024, threads=4, keyLen=32
hashBytes := argon2.IDKey([]byte(password), saltBytes, 1, 64*1024, 4, 32)

return base64.StdEncoding.EncodeToString(hashBytes), base64.StdEncoding.EncodeToString(saltBytes), nil
}
Loading

0 comments on commit 3c3ee31

Please sign in to comment.