Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add http-basic auth reading from a file #573

Merged
merged 5 commits into from
Dec 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/existent/file",
},
},
})

const errorText = "open /some/non/existent/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 @@ -71,6 +71,7 @@ type Proxy struct {
GZIPContentTypes *regexp.Regexp
RequestID string
STSHeader STSHeader
AuthSchemes map[string]AuthScheme
}

type STSHeader struct {
Expand Down Expand Up @@ -161,3 +162,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