Files
dependabot[bot]andUmputun 2e00002413 Bump the go-modules-updates group in /backend with 5 updates
Bumps the go-modules-updates group in /backend with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) | `1.10.0` | `1.10.1` |
| [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.1.0` | `5.2.0` |
| [github.com/go-pkgz/rest](https://github.com/go-pkgz/rest) | `1.19.0` | `1.20.2` |
| [golang.org/x/image](https://github.com/golang/image) | `0.22.0` | `0.23.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.31.0` | `0.33.0` |

Updates `github.com/PuerkitoBio/goquery` from 1.10.0 to 1.10.1
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.10.0...v1.10.1)

Updates `github.com/go-chi/chi/v5` from 5.1.0 to 5.2.0
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.1.0...v5.2.0)

Updates `github.com/go-pkgz/rest` from 1.19.0 to 1.20.2
- [Release notes](https://github.com/go-pkgz/rest/releases)
- [Commits](https://github.com/go-pkgz/rest/compare/v1.19.0...v1.20.2)

Updates `golang.org/x/image` from 0.22.0 to 0.23.0
- [Commits](https://github.com/golang/image/compare/v0.22.0...v0.23.0)

Updates `golang.org/x/net` from 0.31.0 to 0.33.0
- [Commits](https://github.com/golang/net/compare/v0.31.0...v0.33.0)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: github.com/go-chi/chi/v5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/go-pkgz/rest
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/image
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-02 14:02:42 -06:00

164 lines
5.4 KiB
Go

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"
// BasicAuth middleware requires basic auth and matches user & passwd with client-provided checker
func BasicAuth(checker func(user, passwd string) bool) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
if !checker(u, p) {
w.WriteHeader(http.StatusForbidden)
return
}
h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), contextKey(baContextKey), true)))
}
return http.HandlerFunc(fn)
}
}
// BasicAuthWithUserPasswd middleware requires basic auth and matches user & passwd with client-provided values
func BasicAuthWithUserPasswd(user, passwd string) func(http.Handler) http.Handler {
checkFn := func(reqUser, reqPasswd string) bool {
matchUser := subtle.ConstantTimeCompare([]byte(user), []byte(reqUser))
matchPass := subtle.ConstantTimeCompare([]byte(passwd), []byte(reqPasswd))
return matchUser == 1 && matchPass == 1
}
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 {
v := ctx.Value(contextKey(baContextKey))
return v != nil && v.(bool)
}
// BasicAuthWithPrompt middleware requires basic auth and matches user & passwd with client-provided values
// If the user is not authorized, it will prompt for basic auth
func BasicAuthWithPrompt(user, passwd string) func(http.Handler) http.Handler {
checkFn := func(reqUser, reqPasswd string) bool {
matchUser := subtle.ConstantTimeCompare([]byte(user), []byte(reqUser))
matchPass := subtle.ConstantTimeCompare([]byte(passwd), []byte(reqPasswd))
return matchUser == 1 && matchPass == 1
}
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// extract basic auth from request
u, p, ok := r.BasicAuth()
if ok && checkFn(u, p) {
h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), contextKey(baContextKey), true)))
return
}
// not authorized, prompt for basic auth
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
return http.HandlerFunc(fn)
}
}
// BasicAuthWithBcryptHashAndPrompt middleware requires basic auth and matches user & bcrypt hashed password
// If the user is not authorized, it will prompt for basic auth
func BasicAuthWithBcryptHashAndPrompt(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 func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// extract basic auth from request
u, p, ok := r.BasicAuth()
if ok && checkFn(u, p) {
h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), contextKey(baContextKey), true)))
return
}
// not authorized, prompt for basic auth
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
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
}