Add htpasswd-style static file auth module
Can be used via `-auth.url=file://`. Only supports bcrypt password hashes ($2y). Use e.g. `htpasswd -c -BC 14 <filename> <user>` to create a file. Documentation forthcoming.
This commit is contained in:
parent
536f83fa61
commit
a87520cb0f
91
auth/htpasswd.go
Normal file
91
auth/htpasswd.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This provider provides htpasswd style authentication, but _only_ if the
|
||||||
|
// bcrypt algorithm is used (hash must start with $2y). Use e.g.
|
||||||
|
// `htpasswd -c -BC 17 <filename> <user>`
|
||||||
|
|
||||||
|
type htpasswdProvider struct {
|
||||||
|
users map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHtpasswd(location string) (AuthProvider, error) {
|
||||||
|
file, err := os.Open(location)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open %s: %s", location, err.Error())
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var result htpasswdProvider
|
||||||
|
result.users = make(map[string]string)
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Split(scanner.Text(), ":")
|
||||||
|
if len(fields) != 2 {
|
||||||
|
return nil, fmt.Errorf("failed to parse %s: %s: expected 2 fields, found %d", location, scanner.Text(), len(fields))
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(fields[1], "$2y$") {
|
||||||
|
return nil, fmt.Errorf("failed to parse %s: %s is not a bcrypt hash ($2y)", location, scanner.Text())
|
||||||
|
}
|
||||||
|
result.users[fields[0]] = fields[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse %s: %s", location, err.Error())
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prov *htpasswdProvider) Middleware() func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
prov.htpasswdAuth(next, w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prov *htpasswdProvider) htpasswdAuth(next http.Handler, w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, pass, ok := r.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
w.Header().Add("WWW-Authenticate", `Basic realm="Please provide your system credentials", charset="UTF-8"`)
|
||||||
|
http.Error(w, "HTTP Basic auth is required", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, ok := prov.users[user]
|
||||||
|
if !ok {
|
||||||
|
log.Debug().Str("user", user).Msg("auth error")
|
||||||
|
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)); err != nil {
|
||||||
|
if err != bcrypt.ErrMismatchedHashAndPassword {
|
||||||
|
log.Warn().Err(err).Str("user", user).Msg("password check failed")
|
||||||
|
} else {
|
||||||
|
log.Debug().Str("user", user).Msg("auth error")
|
||||||
|
}
|
||||||
|
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authCtx := AuthContext{
|
||||||
|
AuthMethod: "htpasswd",
|
||||||
|
UserName: user,
|
||||||
|
}
|
||||||
|
ctx := NewContext(r.Context(), &authCtx)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
@ -18,6 +18,12 @@ func NewFromURL(authURL string) (AuthProvider, error) {
|
|||||||
return NewIMAP(u.Host, true), nil
|
return NewIMAP(u.Host, true), nil
|
||||||
case "pam":
|
case "pam":
|
||||||
return NewPAM()
|
return NewPAM()
|
||||||
|
case "file":
|
||||||
|
path := u.Path
|
||||||
|
if u.Host != "" {
|
||||||
|
path = u.Host + path
|
||||||
|
}
|
||||||
|
return NewHtpasswd(path)
|
||||||
case "null":
|
case "null":
|
||||||
return NewNull()
|
return NewNull()
|
||||||
default:
|
default:
|
||||||
|
1
go.mod
1
go.mod
@ -11,6 +11,7 @@ require (
|
|||||||
github.com/go-chi/chi/v5 v5.0.10
|
github.com/go-chi/chi/v5 v5.0.10
|
||||||
github.com/msteinert/pam v1.2.0
|
github.com/msteinert/pam v1.2.0
|
||||||
github.com/rs/zerolog v1.31.0
|
github.com/rs/zerolog v1.31.0
|
||||||
|
golang.org/x/crypto v0.18.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
4
go.sum
4
go.sum
@ -30,12 +30,14 @@ github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR
|
|||||||
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
|
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
|
||||||
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
|
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
|
||||||
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
|
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
|
||||||
|
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||||
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
Loading…
Reference in New Issue
Block a user