175 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			175 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Go
		
	
	
	
| package basic
 | |
| 
 | |
| import (
 | |
| 	// We support SHA1 & bcrypt in HTPasswd
 | |
| 	"crypto/sha1" // #nosec G505
 | |
| 	"encoding/base64"
 | |
| 	"encoding/csv"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"sync"
 | |
| 
 | |
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
 | |
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/watcher"
 | |
| 	"golang.org/x/crypto/bcrypt"
 | |
| )
 | |
| 
 | |
| // htpasswdMap represents the structure of an htpasswd file.
 | |
| // Passwords must be generated with -B for bcrypt or -s for SHA1.
 | |
| type htpasswdMap struct {
 | |
| 	users map[string]interface{}
 | |
| 	rwm   sync.RWMutex
 | |
| }
 | |
| 
 | |
| // bcryptPass is used to identify bcrypt passwords in the
 | |
| // htpasswdMap users.
 | |
| type bcryptPass string
 | |
| 
 | |
| // sha1Pass os used to identify sha1 passwords in the
 | |
| // htpasswdMap users.
 | |
| type sha1Pass string
 | |
| 
 | |
| // NewHTPasswdValidator constructs an httpasswd based validator from the file
 | |
| // at the path given.
 | |
| func NewHTPasswdValidator(path string) (Validator, error) {
 | |
| 	h := &htpasswdMap{users: make(map[string]interface{})}
 | |
| 
 | |
| 	if err := h.loadHTPasswdFile(path); err != nil {
 | |
| 		return nil, fmt.Errorf("could not load htpasswd file: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if err := watcher.WatchFileForUpdates(path, nil, func() {
 | |
| 		err := h.loadHTPasswdFile(path)
 | |
| 		if err != nil {
 | |
| 			logger.Errorf("%v: no changes were made to the current htpasswd map", err)
 | |
| 		}
 | |
| 	}); err != nil {
 | |
| 		return nil, fmt.Errorf("could not watch htpasswd file: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return h, nil
 | |
| }
 | |
| 
 | |
| // loadHTPasswdFile loads htpasswd entries from an io.Reader (an opened file) into a htpasswdMap.
 | |
| func (h *htpasswdMap) loadHTPasswdFile(filename string) error {
 | |
| 	// We allow HTPasswd location via config options
 | |
| 	r, err := os.Open(filename) // #nosec G304
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("could not open htpasswd file: %v", err)
 | |
| 	}
 | |
| 	defer func(c io.Closer) {
 | |
| 		cerr := c.Close()
 | |
| 		if cerr != nil {
 | |
| 			logger.Fatalf("error closing the htpasswd file: %v", cerr)
 | |
| 		}
 | |
| 	}(r)
 | |
| 
 | |
| 	csvReader := csv.NewReader(r)
 | |
| 	csvReader.Comma = ':'
 | |
| 	csvReader.Comment = '#'
 | |
| 	csvReader.TrimLeadingSpace = true
 | |
| 
 | |
| 	records, err := csvReader.ReadAll()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("could not read htpasswd file: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	updated, err := createHtpasswdMap(records)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("htpasswd entries error: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	h.rwm.Lock()
 | |
| 	h.users = updated.users
 | |
| 	h.rwm.Unlock()
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // createHtpasswdMap constructs an htpasswdMap from the given records
 | |
| func createHtpasswdMap(records [][]string) (*htpasswdMap, error) {
 | |
| 	h := &htpasswdMap{users: make(map[string]interface{})}
 | |
| 	var invalidRecords, invalidEntries []string
 | |
| 	for _, record := range records {
 | |
| 		// If a record is invalid or malformed don't panic with index out of range,
 | |
| 		// return a formatted error.
 | |
| 		lr := len(record)
 | |
| 		switch {
 | |
| 		case lr == 2:
 | |
| 			user, realPassword := record[0], record[1]
 | |
| 			invalidEntries = passShaOrBcrypt(h, user, realPassword)
 | |
| 		case lr == 1, lr > 2:
 | |
| 			invalidRecords = append(invalidRecords, record[0])
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(invalidRecords) > 0 {
 | |
| 		return h, fmt.Errorf("invalid htpasswd record(s) %+q", invalidRecords)
 | |
| 	}
 | |
| 
 | |
| 	if len(invalidEntries) > 0 {
 | |
| 		return h, fmt.Errorf("'%+q' user(s) could not be added: invalid password, must be a SHA or bcrypt entry", invalidEntries)
 | |
| 	}
 | |
| 
 | |
| 	if len(h.users) == 0 {
 | |
| 		return nil, fmt.Errorf("could not construct htpasswdMap: htpasswd file doesn't contain a single valid user entry")
 | |
| 	}
 | |
| 
 | |
| 	return h, nil
 | |
| }
 | |
| 
 | |
| // passShaOrBcrypt checks if a htpasswd entry is valid and the password is encrypted with SHA or bcrypt.
 | |
| // Valid user entries are saved in the htpasswdMap, invalid records are reurned.
 | |
| func passShaOrBcrypt(h *htpasswdMap, user, password string) (invalidEntries []string) {
 | |
| 	passLen := len(password)
 | |
| 	switch {
 | |
| 	case passLen > 6 && password[:5] == "{SHA}":
 | |
| 		h.users[user] = sha1Pass(password[5:])
 | |
| 	case passLen > 5 &&
 | |
| 		(password[:4] == "$2b$" ||
 | |
| 			password[:4] == "$2y$" ||
 | |
| 			password[:4] == "$2x$" ||
 | |
| 			password[:4] == "$2a$"):
 | |
| 		h.users[user] = bcryptPass(password)
 | |
| 	default:
 | |
| 		invalidEntries = append(invalidEntries, user)
 | |
| 	}
 | |
| 
 | |
| 	return invalidEntries
 | |
| }
 | |
| 
 | |
| // GetUsers return a "thread safe" copy of the internal user list
 | |
| func (h *htpasswdMap) GetUsers() map[string]interface{} {
 | |
| 	newUserList := make(map[string]interface{})
 | |
| 	h.rwm.Lock()
 | |
| 	for key, value := range h.users {
 | |
| 		newUserList[key] = value
 | |
| 	}
 | |
| 	h.rwm.Unlock()
 | |
| 	return newUserList
 | |
| }
 | |
| 
 | |
| // Validate checks a users password against the htpasswd entries
 | |
| func (h *htpasswdMap) Validate(user string, password string) bool {
 | |
| 	realPassword, exists := h.users[user]
 | |
| 	if !exists {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	switch rp := realPassword.(type) {
 | |
| 	case sha1Pass:
 | |
| 		// We support SHA1 HTPasswd entries
 | |
| 		d := sha1.New() // #nosec G401
 | |
| 		_, err := d.Write([]byte(password))
 | |
| 		if err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		return string(rp) == base64.StdEncoding.EncodeToString(d.Sum(nil))
 | |
| 	case bcryptPass:
 | |
| 		return bcrypt.CompareHashAndPassword([]byte(rp), []byte(password)) == nil
 | |
| 	default:
 | |
| 		return false
 | |
| 	}
 | |
| }
 |