Skip to content

Commit

Permalink
add http-basic auth reading from a htpasswd file
Browse files Browse the repository at this point in the history
  • Loading branch information
andyroyle committed Nov 19, 2018
1 parent fea29da commit 8374e1b
Show file tree
Hide file tree
Showing 21 changed files with 728 additions and 5 deletions.
30 changes: 30 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package auth

import (
"fmt"
"net/http"

"github.com/fabiolb/fabio/config"
)

type AuthScheme interface {
Authorized(request *http.Request, response http.ResponseWriter) bool
}

func LoadAuthSchemes(cfg map[string]config.AuthScheme) (map[string]AuthScheme, error) {
auths := map[string]AuthScheme{}
for _, a := range cfg {
switch a.Type {
case "basic":
b, err := newBasicAuth(a.Basic)
if err != nil {
return nil, err
}
auths[a.Name] = b
default:
return nil, fmt.Errorf("unknown auth type '%s'", a.Type)
}
}

return auths, nil
}
76 changes: 76 additions & 0 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package auth

import (
"testing"

"github.com/fabiolb/fabio/config"
)

func TestLoadAuthSchemes(t *testing.T) {

t.Run("should fail when auth scheme fails to load", func(t *testing.T) {
_, err := LoadAuthSchemes(map[string]config.AuthScheme{
"myauth": {
Name: "myauth",
Type: "basic",
Basic: config.BasicAuth{
File: "/some/non/existant/file",
},
},
})

const errorText = "open /some/non/existant/file: no such file or directory"

if err.Error() != errorText {
t.Fatalf("got %s, want %s", err.Error(), errorText)
}
})

t.Run("should return an error when auth type is unknown", func(t *testing.T) {
_, err := LoadAuthSchemes(map[string]config.AuthScheme{
"myauth": {
Name: "myauth",
Type: "foo",
},
})

const errorText = "unknown auth type 'foo'"

if err.Error() != errorText {
t.Fatalf("got %s, want %s", err.Error(), errorText)
}
})

t.Run("should load multiple auth schemes", func(t *testing.T) {
myauth, err := createBasicAuthFile("foo:bar")
if err != nil {
t.Fatalf("could not create file on disk %s", err)
}

myotherauth, err := createBasicAuthFile("bar:foo")
if err != nil {
t.Fatalf("could not create file on disk %s", err)
}

result, err := LoadAuthSchemes(map[string]config.AuthScheme{
"myauth": {
Name: "myauth",
Type: "basic",
Basic: config.BasicAuth{
File: myauth,
},
},
"myotherauth": {
Name: "myotherauth",
Type: "basic",
Basic: config.BasicAuth{
File: myotherauth,
},
},
})

if len(result) != 2 {
t.Fatalf("expected 2 auth schemes, got %d", len(result))
}
})
}
41 changes: 41 additions & 0 deletions auth/basic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package auth

import (
"log"
"net/http"

"github.com/fabiolb/fabio/config"
"github.com/tg123/go-htpasswd"
)

// basic is an implementation of AuthScheme
type basic struct {
realm string
secrets *htpasswd.HtpasswdFile
}

func newBasicAuth(cfg config.BasicAuth) (AuthScheme, error) {
secrets, err := htpasswd.New(cfg.File, htpasswd.DefaultSystems, func(err error) {
log.Println("[WARN] Error reading Htpasswd file: ", err)
})

if err != nil {
return nil, err
}

return &basic{
secrets: secrets,
realm: cfg.Realm,
}, nil
}

func (b *basic) Authorized(request *http.Request, response http.ResponseWriter) bool {
user, password, ok := request.BasicAuth()

if !ok {
response.Header().Set("WWW-Authenticate", "Basic realm=\""+b.realm+"\"")
return false
}

return b.secrets.Match(user, password)
}
188 changes: 188 additions & 0 deletions auth/basic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package auth

import (
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"

"github.com/fabiolb/fabio/config"
"github.com/fabiolb/fabio/uuid"
)

type responseWriter struct {
header http.Header
code int
written []byte
}

func (rw *responseWriter) Header() http.Header {
if rw.header == nil {
rw.header = map[string][]string{}
}
return rw.header
}

func (rw *responseWriter) Write(b []byte) (int, error) {
rw.written = append(rw.written, b...)
return len(rw.written), nil
}

func (rw *responseWriter) WriteHeader(statusCode int) {
rw.code = statusCode
}

func createBasicAuthFile(contents string) (string, error) {
dir, err := ioutil.TempDir("", "basicauth")

if err != nil {
return "", fmt.Errorf("could not create temp dir: %s", err)
}

filename := fmt.Sprintf("%s/%s", dir, uuid.NewUUID())

err = ioutil.WriteFile(filename, []byte(contents), 0666)

if err != nil {
return "", fmt.Errorf("could not write password file: %s", err)
}

return filename, nil
}

func createBasicAuth(user string, password string) (AuthScheme, error) {
contents := fmt.Sprintf("%s:%s", user, password)

filename, err := createBasicAuthFile(contents)

a, err := newBasicAuth(config.BasicAuth{
File: filename,
Realm: "testrealm",
})

if err != nil {
return nil, fmt.Errorf("could not create basic auth: %s", err)
}

return a, nil
}

func TestNewBasicAuth(t *testing.T) {

t.Run("should create a basic auth scheme from the supplied config", func(t *testing.T) {
filename, err := createBasicAuthFile("foo:bar")

if err != nil {
t.Error(err)
}

_, err = newBasicAuth(config.BasicAuth{
File: filename,
})

if err != nil {
t.Error(err)
}
})

t.Run("should log a warning when credentials are malformed", func(t *testing.T) {
filename, err := createBasicAuthFile("foosdlijdgohdgdbar")

if err != nil {
t.Error(err)
}

_, err = newBasicAuth(config.BasicAuth{
File: filename,
})

if err != nil {
t.Error(err)
}
})
}

func TestBasic_Authorised(t *testing.T) {
basicAuth, err := createBasicAuth("foo", "bar")
creds := []byte("foo:bar")

if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
req *http.Request
res http.ResponseWriter
out bool
}{
{
"correct credentials should be authorized",
&http.Request{
Header: http.Header{
"Authorization": []string{fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(creds))},
},
},
&responseWriter{},
true,
},
{
"incorrect credentials should not be authorized",
&http.Request{
Header: http.Header{
"Authorization": []string{fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte("baz:blarg")))},
},
},
&responseWriter{},
false,
},
{
"missing Authorization header should not be authorized",
&http.Request{
Header: http.Header{},
},
&responseWriter{},
false,
},
{
"malformed Authorization header should not be authorized",
&http.Request{
Header: http.Header{
"Authorization": []string{"malformed"},
},
},
&responseWriter{},
false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, want := basicAuth.Authorized(tt.req, tt.res), tt.out; !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
})
}
}

func TestBasic_Authorized_should_set_www_realm_header(t *testing.T) {
basicAuth, err := createBasicAuth("foo", "bar")

if err != nil {
t.Fatal(err)
}

rw := &responseWriter{}

_ = basicAuth.Authorized(&http.Request{Header: http.Header{}}, rw)

got := rw.Header().Get("WWW-Authenticate")
want := "Basic realm=\"testrealm\""

if strings.Compare(got, want) != 0 {
t.Errorf("got '%s', want '%s'", got, want)
}
}
12 changes: 12 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type Proxy struct {
GZIPContentTypes *regexp.Regexp
RequestID string
STSHeader STSHeader
AuthSchemes map[string]AuthScheme
}

type STSHeader struct {
Expand Down Expand Up @@ -158,3 +159,14 @@ type Tracing struct {
SamplerRate float64
SpanHost string
}

type AuthScheme struct {
Name string
Type string
Basic BasicAuth
}

type BasicAuth struct {
Realm string
File string
}
2 changes: 2 additions & 0 deletions config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
var defaultValues = struct {
ListenerValue string
CertSourcesValue string
AuthSchemesValue string
ReadTimeout time.Duration
WriteTimeout time.Duration
UIListenerValue string
Expand Down Expand Up @@ -44,6 +45,7 @@ var defaultConfig = &Config{
FlushInterval: time.Second,
GlobalFlushInterval: 0,
LocalIP: LocalIPString(),
AuthSchemes: map[string]AuthScheme{},
},
Registry: Registry{
Backend: "consul",
Expand Down
Loading

0 comments on commit 8374e1b

Please sign in to comment.