Repository: bitly/oauth2_proxy (go)

Issue:
Htpasswd using an insecure hashing algorithm
In your example configuration file it says that you have to use the SHA encryption. Using SHA is not recommend due to not being designed for storing password. Instead it is recommended to use one of the algorithms designed for passwords. Htpasswd has support for the more secure bcrypt. 

> \#\# Additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption 

From htpasswd usage:

> -B  Force bcrypt encryption of the password (very secure).
> -s  Force SHA encryption of the password (insecure).

Pull request description:
Support bcrypt passwords in htpasswd
Fixes #404 (at least partially)

Patch:
diff --git a/Gopkg.lock b/Gopkg.lock
index f447496..5a3758a 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -58,7 +58,10 @@
 [[projects]]
   branch = "master"
   name = "github.com/pquerna/cachecontrol"
-  packages = [".","cacheobject"]
+  packages = [
+    ".",
+    "cacheobject"
+  ]
   revision = "0dec1b30a0215bb68605dfc568e8855066c9202d"
 
 [[projects]]
@@ -70,30 +73,60 @@
 [[projects]]
   branch = "master"
   name = "golang.org/x/crypto"
-  packages = ["ed25519","ed25519/internal/edwards25519"]
+  packages = [
+    "bcrypt",
+    "blowfish",
+    "ed25519",
+    "ed25519/internal/edwards25519"
+  ]
   revision = "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94"
 
 [[projects]]
   branch = "master"
   name = "golang.org/x/net"
-  packages = ["context","context/ctxhttp"]
+  packages = [
+    "context",
+    "context/ctxhttp"
+  ]
   revision = "9dfe39835686865bff950a07b394c12a98ddc811"
 
 [[projects]]
   branch = "master"
   name = "golang.org/x/oauth2"
-  packages = [".","google","internal","jws","jwt"]
+  packages = [
+    ".",
+    "google",
+    "internal",
+    "jws",
+    "jwt"
+  ]
   revision = "9ff8ebcc8e241d46f52ecc5bff0e5a2f2dbef402"
 
 [[projects]]
   branch = "master"
   name = "google.golang.org/api"
-  packages = ["admin/directory/v1","gensupport","googleapi","googleapi/internal/uritemplates"]
+  packages = [
+    "admin/directory/v1",
+    "gensupport",
+    "googleapi",
+    "googleapi/internal/uritemplates"
+  ]
   revision = "8791354e7ab150705ede13637a18c1fcc16b62e8"
 
 [[projects]]
   name = "google.golang.org/appengine"
-  packages = [".","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"]
+  packages = [
+    ".",
+    "internal",
+    "internal/app_identity",
+    "internal/base",
+    "internal/datastore",
+    "internal/log",
+    "internal/modules",
+    "internal/remote_api",
+    "internal/urlfetch",
+    "urlfetch"
+  ]
   revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
   version = "v1.0.0"
 
@@ -105,13 +138,17 @@
 
 [[projects]]
   name = "gopkg.in/square/go-jose.v2"
-  packages = [".","cipher","json"]
+  packages = [
+    ".",
+    "cipher",
+    "json"
+  ]
   revision = "f8f38de21b4dcd69d0413faf231983f5fd6634b1"
   version = "v2.1.3"
 
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "efab48a0e196c2a849bfbe9aa02d2ae28d281ce1bfe9f23720d648858eefc8e6"
+  inputs-digest = "b502c41a61115d14d6379be26b0300f65d173bdad852f0170d387ebf2d7ec173"
   solver-name = "gps-cdcl"
   solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
index 97f83d0..c4005e1 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -38,3 +38,7 @@
 [[constraint]]
   name = "gopkg.in/fsnotify.v1"
   version = "~1.2.0"
+
+[[constraint]]
+  branch = "master"
+  name = "golang.org/x/crypto"
diff --git a/htpasswd.go b/htpasswd.go
index 6fd888e..c68e558 100644
--- a/htpasswd.go
+++ b/htpasswd.go
@@ -7,10 +7,12 @@ import (
 	"io"
 	"log"
 	"os"
+
+	"golang.org/x/crypto/bcrypt"
 )
 
-// lookup passwords in a htpasswd file
-// The entries must have been created with -s for SHA encryption
+// Lookup passwords in a htpasswd file
+// Passwords must be generated with -B for bcrypt or -s for SHA1.
 
 type HtpasswdFile struct {
 	Users map[string]string
@@ -47,14 +49,20 @@ func (h *HtpasswdFile) Validate(user string, password string) bool {
 	if !exists {
 		return false
 	}
-	if realPassword[:5] == "{SHA}" {
+
+	shaPrefix := realPassword[:5]
+	if shaPrefix == "{SHA}" {
+		shaValue := realPassword[5:]
 		d := sha1.New()
 		d.Write([]byte(password))
-		if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
-			return true
-		}
-	} else {
-		log.Printf("Invalid htpasswd entry for %s. Must be a SHA entry.", user)
+		return shaValue == base64.StdEncoding.EncodeToString(d.Sum(nil))
 	}
+
+	bcryptPrefix := realPassword[:4]
+	if bcryptPrefix == "$2a$" || bcryptPrefix == "$2b$" || bcryptPrefix == "$2x$" || bcryptPrefix == "$2y$" {
+		return bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password)) == nil
+	}
+
+	log.Printf("Invalid htpasswd entry for %s. Must be a SHA or bcrypt entry.", user)
 	return false
 }
diff --git a/main.go b/main.go
index 4e4c7ed..287dc48 100644
--- a/main.go
+++ b/main.go
@@ -52,7 +52,7 @@ func main() {
 	flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
 	flagSet.String("client-secret", "", "the OAuth Client Secret")
 	flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)")
-	flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption")
+	flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -s\" for SHA encryption or \"htpasswd -B\" for bcrypt encryption")
 	flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided")
 	flagSet.String("custom-templates-dir", "", "path to custom html templates")
 	flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")


Test patch:
diff --git a/htpasswd_test.go b/htpasswd_test.go
index 17ce37b..ebfd503 100644
--- a/htpasswd_test.go
+++ b/htpasswd_test.go
@@ -2,11 +2,14 @@ package main
 
 import (
 	"bytes"
-	"github.com/stretchr/testify/assert"
+	"fmt"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"golang.org/x/crypto/bcrypt"
 )
 
-func TestHtpasswd(t *testing.T) {
+func TestSHA(t *testing.T) {
 	file := bytes.NewBuffer([]byte("testuser:{SHA}PaVBVZkYqAjCQCu6UBL2xgsnZhw=\n"))
 	h, err := NewHtpasswd(file)
 	assert.Equal(t, err, nil)
@@ -14,3 +17,21 @@ func TestHtpasswd(t *testing.T) {
 	valid := h.Validate("testuser", "asdf")
 	assert.Equal(t, valid, true)
 }
+
+func TestBcrypt(t *testing.T) {
+	hash1, err := bcrypt.GenerateFromPassword([]byte("password"), 1)
+	hash2, err := bcrypt.GenerateFromPassword([]byte("top-secret"), 2)
+	assert.Equal(t, err, nil)
+
+	contents := fmt.Sprintf("testuser1:%s\ntestuser2:%s\n", hash1, hash2)
+	file := bytes.NewBuffer([]byte(contents))
+
+	h, err := NewHtpasswd(file)
+	assert.Equal(t, err, nil)
+
+	valid := h.Validate("testuser1", "password")
+	assert.Equal(t, valid, true)
+
+	valid = h.Validate("testuser2", "top-secret")
+	assert.Equal(t, valid, true)
+}