@@ -8,8 +8,10 @@ import (
88 "fmt"
99 "io"
1010 "os"
11+ "sync"
1112
1213 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/watcher"
1315 "golang.org/x/crypto/bcrypt"
1416)
1517
@@ -17,6 +19,7 @@ import (
1719// Passwords must be generated with -B for bcrypt or -s for SHA1.
1820type htpasswdMap struct {
1921 users map[string]interface{}
22+ rwm sync.RWMutex
2023}
2124
2225// bcryptPass is used to identify bcrypt passwords in the
@@ -30,10 +33,30 @@ type sha1Pass string
3033// NewHTPasswdValidator constructs an httpasswd based validator from the file
3134// at the path given.
3235func NewHTPasswdValidator(path string) (Validator, error) {
36+ h := &htpasswdMap{users: make(map[string]interface{})}
37+
38+ if err := h.loadHTPasswdFile(path); err != nil {
39+ return nil, fmt.Errorf("could not load htpasswd file: %v", err)
40+ }
41+
42+ if err := watcher.WatchFileForUpdates(path, nil, func() {
43+ err := h.loadHTPasswdFile(path)
44+ if err != nil {
45+ logger.Errorf("%v: no changes were made to the current htpasswd map", err)
46+ }
47+ }); err != nil {
48+ return nil, fmt.Errorf("could not watch htpasswd file: %v", err)
49+ }
50+
51+ return h, nil
52+}
53+
54+// loadHTPasswdFile loads htpasswd entries from an io.Reader (an opened file) into a htpasswdMap.
55+func (h *htpasswdMap) loadHTPasswdFile(filename string) error {
3356 // We allow HTPasswd location via config options
34− r, err := os.Open(path) // #nosec G304
57+ r, err := os.Open(filename) // #nosec G304
3558 if err != nil {
36− return nil, fmt.Errorf("could not open htpasswd file: %v", err)
59+ return fmt.Errorf("could not open htpasswd file: %v", err)
3760 }
3861 defer func(c io.Closer) {
3962 cerr := c.Close()
@@ -41,48 +64,81 @@ func NewHTPasswdValidator(path string) (Validator, error) {
4164 logger.Fatalf("error closing the htpasswd file: %v", cerr)
4265 }
4366 }(r)
44− return newHtpasswd(r)
45−}
4667
47−// newHtpasswd consctructs an htpasswd from an io.Reader (an opened file).
48−func newHtpasswd(file io.Reader) (*htpasswdMap, error) {
49− csvReader := csv.NewReader(file)
68+ csvReader := csv.NewReader(r)
5069 csvReader.Comma = ':'
5170 csvReader.Comment = '#'
5271 csvReader.TrimLeadingSpace = true
5372
5473 records, err := csvReader.ReadAll()
5574 if err != nil {
56− return nil, fmt.Errorf("could not read htpasswd file: %v", err)
75+ return fmt.Errorf("could not read htpasswd file: %v", err)
5776 }
5877
59− return createHtpasswdMap(records)
78+ updated, err := createHtpasswdMap(records)
79+ if err != nil {
80+ return fmt.Errorf("htpasswd entries error: %v", err)
81+ }
82+
83+ h.rwm.Lock()
84+ h.users = updated.users
85+ h.rwm.Unlock()
86+
87+ return nil
6088}
6189
6290// createHtasswdMap constructs an htpasswdMap from the given records
6391func createHtpasswdMap(records [][]string) (*htpasswdMap, error) {
6492 h := &htpasswdMap{users: make(map[string]interface{})}
93+ var invalidRecords, invalidEntries []string
6594 for _, record := range records {
66− user, realPassword := record[0], record[1]
67− shaPrefix := realPassword[:5]
68− if shaPrefix == "{SHA}" {
69− h.users[user] = sha1Pass(realPassword[5:])
70− continue
95+ // If a record is invalid or malformed don't panic with index out of range,
96+ // return a formatted error.
97+ lr := len(record)
98+ switch {
99+ case lr == 2:
100+ user, realPassword := record[0], record[1]
101+ invalidEntries = passShaOrBcrypt(h, user, realPassword)
102+ case lr == 1, lr > 2:
103+ invalidRecords = append(invalidRecords, record[0])
71104 }
105+ }
72106
73− bcryptPrefix := realPassword[:4]
74− if bcryptPrefix == "$2a$" || bcryptPrefix == "$2b$" || bcryptPrefix == "$2x$" || bcryptPrefix == "$2y$" {
75− h.users[user] = bcryptPass(realPassword)
76− continue
77− }
107+ if len(invalidRecords) > 0 {
108+ return h, fmt.Errorf("invalid htpasswd record(s) %+q", invalidRecords)
109+ }
110+
111+ if len(invalidEntries) > 0 {
112+ return h, fmt.Errorf("'%+q' user(s) could not be added: invalid password, must be a SHA or bcrypt entry", invalidEntries)
113+ }
78114
79− // Password is neither sha1 or bcrypt
80− // TODO(JoelSpeed): In the next breaking release, make this return an error.
81− logger.Errorf("Invalid htpasswd entry for %s. Must be a SHA or bcrypt entry.", user)
115+ if len(h.users) == 0 {
116+ return nil, fmt.Errorf("could not construct htpasswdMap: htpasswd file doesn't contain a single valid user entry")
82117 }
118+
83119 return h, nil
84120}
85121
122+// passShaOrBcrypt checks if a htpasswd entry is valid and the password is encrypted with SHA or bcrypt.
123+// Valid user entries are saved in the htpasswdMap, invalid records are reurned.
124+func passShaOrBcrypt(h *htpasswdMap, user, password string) (invalidEntries []string) {
125+ passLen := len(password)
126+ switch {
127+ case passLen > 6 && password[:5] == "{SHA}":
128+ h.users[user] = sha1Pass(password[5:])
129+ case passLen > 5 &&
130+ (password[:4] == "$2b$" ||
131+ password[:4] == "$2y$" ||
132+ password[:4] == "$2x$" ||
133+ password[:4] == "$2a$"):
134+ h.users[user] = bcryptPass(password)
135+ default:
136+ invalidEntries = append(invalidEntries, user)
137+ }
138+
139+ return invalidEntries
140+}
141+
86142// Validate checks a users password against the htpasswd entries
87143func (h *htpasswdMap) Validate(user string, password string) bool {
88144 realPassword, exists := h.users[user]