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 ` 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) }