1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-03-25 22:00:56 +02:00

Add HTPasswdValidator to basic authentication package

This commit is contained in:
Joel Speed 2020-07-18 00:41:42 +01:00
parent 895403cb9b
commit 7d8ee61254
No known key found for this signature in database
GPG Key ID: 6E80578D6751DEFB
7 changed files with 239 additions and 0 deletions

@ -0,0 +1,16 @@
package basic
import (
"testing"
"github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestBasicSuite(t *testing.T) {
logger.SetOutput(GinkgoWriter)
RegisterFailHandler(Fail)
RunSpecs(t, "Basic")
}

@ -0,0 +1,96 @@
package basic
import (
"crypto/sha1"
"encoding/base64"
"encoding/csv"
"fmt"
"io"
"os"
"github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
"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{}
}
// 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) {
r, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("could not open htpasswd file: %v", err)
}
defer r.Close()
return newHtpasswd(r)
}
// newHtpasswd consctructs an htpasswd from an io.Reader (an opened file).
func newHtpasswd(file io.Reader) (*htpasswdMap, error) {
csvReader := csv.NewReader(file)
csvReader.Comma = ':'
csvReader.Comment = '#'
csvReader.TrimLeadingSpace = true
records, err := csvReader.ReadAll()
if err != nil {
return nil, fmt.Errorf("could not read htpasswd file: %v", err)
}
return createHtpasswdMap(records)
}
// createHtasswdMap constructs an htpasswdMap from the given records
func createHtpasswdMap(records [][]string) (*htpasswdMap, error) {
h := &htpasswdMap{users: make(map[string]interface{})}
for _, record := range records {
user, realPassword := record[0], record[1]
shaPrefix := realPassword[:5]
if shaPrefix == "{SHA}" {
h.users[user] = sha1Pass(realPassword[5:])
continue
}
bcryptPrefix := realPassword[:4]
if bcryptPrefix == "$2a$" || bcryptPrefix == "$2b$" || bcryptPrefix == "$2x$" || bcryptPrefix == "$2y$" {
h.users[user] = bcryptPass(realPassword)
continue
}
// Password is neither sha1 or bcrypt
// TODO(JoelSpeed): In the next breaking release, make this return an error.
logger.Printf("Invalid htpasswd entry for %s. Must be a SHA or bcrypt entry.", user)
}
return h, nil
}
// 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 real := realPassword.(type) {
case sha1Pass:
d := sha1.New()
d.Write([]byte(password))
return string(real) == base64.StdEncoding.EncodeToString(d.Sum(nil))
case bcryptPass:
return bcrypt.CompareHashAndPassword([]byte(real), []byte(password)) == nil
default:
return false
}
}

@ -0,0 +1,96 @@
package basic
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
const (
adminUser = "admin"
adminPassword = "Adm1n1str$t0r"
user1 = "user1"
user1Password = "UsErOn3P455"
user2 = "user2"
user2Password = "us3r2P455W0Rd!"
)
var _ = Describe("HTPasswd Suite", func() {
Context("with an HTPassword Validator", func() {
assertHtpasswdMapFromFile := func(filePath string) {
var htpasswd *htpasswdMap
var err error
BeforeEach(func() {
var validator Validator
validator, err = NewHTPasswdValidator(filePath)
var ok bool
htpasswd, ok = validator.(*htpasswdMap)
Expect(ok).To(BeTrue())
})
It("does not return an error", func() {
Expect(err).ToNot(HaveOccurred())
})
It("has the correct number of users", func() {
Expect(htpasswd.users).To(HaveLen(3))
})
It("accepts the correct passwords", func() {
Expect(htpasswd.Validate(adminUser, adminPassword)).To(BeTrue())
Expect(htpasswd.Validate(user1, user1Password)).To(BeTrue())
Expect(htpasswd.Validate(user2, user2Password)).To(BeTrue())
})
It("rejects incorrect passwords", func() {
Expect(htpasswd.Validate(adminUser, "asvdfda")).To(BeFalse())
Expect(htpasswd.Validate(user1, "BHEdgbtr")).To(BeFalse())
Expect(htpasswd.Validate(user2, "12345")).To(BeFalse())
})
It("rejects a non existent user", func() {
// Users are case sensitive
Expect(htpasswd.Validate("ADMIN", adminPassword)).To(BeFalse())
})
}
Context("load from file", func() {
Context("with sha1 entries", func() {
const filePath = "./test/htpasswd-sha1.txt"
assertHtpasswdMapFromFile(filePath)
})
Context("with bcrypt entries", func() {
const filePath = "./test/htpasswd-bcrypt.txt"
assertHtpasswdMapFromFile(filePath)
})
Context("with mixed entries", func() {
const filePath = "./test/htpasswd-mixed.txt"
assertHtpasswdMapFromFile(filePath)
})
Context("with a non existent file", func() {
const filePath = "./test/htpasswd-doesnt-exist.txt"
var validator Validator
var err error
BeforeEach(func() {
validator, err = NewHTPasswdValidator(filePath)
})
It("returns an error", func() {
Expect(err).To(MatchError("could not open htpasswd file: open ./test/htpasswd-doesnt-exist.txt: no such file or directory"))
})
It("returns a nil validator", func() {
Expect(validator).To(BeNil())
})
})
})
})
})

@ -0,0 +1,8 @@
# admin:Adm1n1str$t0r
admin:$2y$05$SXWrNM7ldtbRzBvUC3VXyOvUeiUcP45XPwM93P5eeGOEPIiAZmJjC
# user1:UsErOn3P455
user1:$2y$05$/sZYJOk8.3Etg4V6fV7puuXfCJLmV5Q7u3xvKpjBSJUka.t2YtmmG
# user2: us3r2P455W0Rd!
user2:$2y$05$l22MubgKTZFTjTs8TNg5k.YKvcnM2.bA/.iwl0idef5CbekdvBxva

@ -0,0 +1,8 @@
# admin:Adm1n1str$t0r
admin:$2y$05$SXWrNM7ldtbRzBvUC3VXyOvUeiUcP45XPwM93P5eeGOEPIiAZmJjC
# user1:UsErOn3P455
user1:{SHA}Dvs/L78raajL4jEAHPkwflQXJzI=
# user2: us3r2P455W0Rd!
user2:{SHA}MoN9/JCJEcYUb6GCQ+2buDvn9pI=

@ -0,0 +1,8 @@
# admin:Adm1n1str$t0r
admin:{SHA}gXQeRH0bcaCfhAk2gOLm1uaePMA=
# user1:UsErOn3P455
user1:{SHA}Dvs/L78raajL4jEAHPkwflQXJzI=
# user2: us3r2P455W0Rd!
user2:{SHA}MoN9/JCJEcYUb6GCQ+2buDvn9pI=

@ -0,0 +1,7 @@
package basic
// Validator is a minimal interface for something that can validate a
// username and password combination.
type Validator interface {
Validate(user, password string) bool
}