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

Improved basic auth htpasswd file refresh #604 #610

Merged
merged 7 commits into from
Apr 4, 2019
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
28 changes: 20 additions & 8 deletions auth/basic.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package auth

import (
"bytes"
"log"
"net/http"
"os"
Expand All @@ -13,33 +14,43 @@ import (
// basic is an implementation of AuthScheme
type basic struct {
realm string
secrets *htpasswd.HtpasswdFile
secrets *htpasswd.File
}

func newBasicAuth(cfg config.BasicAuth) (AuthScheme, error) {
bad := func(err error) {
log.Println("[WARN] Error processing a line in an htpasswd file:", err)
}

stat, err := os.Stat(cfg.File)
if err != nil {
return nil, err
}
cfg.ModTime = stat.ModTime()

secrets, err := htpasswd.New(cfg.File, htpasswd.DefaultSystems, bad)
if err != nil {
return nil, err
}

if cfg.Refresh > 0 {
stat, err := os.Stat(cfg.File)
if err != nil {
return nil, err
}
cfg.ModTime = stat.ModTime()

go func() {
cleared := false
ticker := time.NewTicker(cfg.Refresh).C
for range ticker {
stat, err := os.Stat(cfg.File)
if err != nil {
log.Println("[WARN] Error accessing htpasswd file:", err)
continue // to prevent nil pointer dereference below
if !cleared {
err = secrets.ReloadFromReader(&bytes.Buffer{}, bad)
if err != nil {
log.Println("[WARN] Error clearing the htpasswd credentials:", err)
} else {
log.Println("[INFO] The htpasswd credentials have been cleared")
cleared = true
}
}
continue
}

// refresh the htpasswd file only if its modification time has changed
Expand All @@ -48,6 +59,7 @@ func newBasicAuth(cfg config.BasicAuth) (AuthScheme, error) {
if err := secrets.Reload(bad); err == nil {
log.Println("[INFO] The htpasswd file has been successfully reloaded")
cfg.ModTime = stat.ModTime()
cleared = false
} else {
log.Println("[WARN] Error reloading htpasswd file:", err)
}
Expand Down
44 changes: 44 additions & 0 deletions auth/basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"fmt"
"io/ioutil"
"net/http"
"os"
"reflect"
"strings"
"testing"
"time"

"github.com/fabiolb/fabio/config"
"github.com/fabiolb/fabio/uuid"
Expand Down Expand Up @@ -168,6 +170,48 @@ func TestBasic_Authorised(t *testing.T) {
}
}

func TestBasic_Authorised_should_fail_without_htpasswd_file(t *testing.T) {
filename, err := createBasicAuthFile("foo:bar")
if err != nil {
t.Error(err)
}

a, err := newBasicAuth(config.BasicAuth{
File: filename,
Refresh: time.Second,
})
if err != nil {
t.Error(err)
}

creds := []byte("foo:bar")
r := &http.Request{
Header: http.Header{
"Authorization": []string{fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(creds))},
},
}

w := &responseWriter{}

t.Run("should authorize against supplied htpasswd file", func(t *testing.T) {
if got, want := a.Authorized(r, w), true; !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
})

if err := os.Remove(filename); err != nil {
t.Fatalf("removing htpasswd file: %s", err)
}

time.Sleep(2 * time.Second) // ensure htpasswd file refresh happend

t.Run("should not authorize after removing htpasswd file", func(t *testing.T) {
if got, want := a.Authorized(r, w), false; !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")

Expand Down
1 change: 1 addition & 0 deletions docs/content/feature/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ At the end you also find a list of [examples](#examples).
The basic authorization scheme leverages [Http Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) and reads a [htpasswd](https://httpd.apache.org/docs/2.4/misc/password_encryptions.html) file at startup and credentials are cached until the service exits.

The `file` option contains the path to the htpasswd file. The `realm` parameter is optional (default is to use the `name`). The `refresh` option can set the htpasswd file refresh interval. Minimal refresh interval is `1s` to void busy loop. By default refresh is disabled i.e. set to zero.
Note: removing the htpasswd file will cause all requests to fail with HTTP status code 401 (Unauthorized) until the file is restored.

name=<name>;type=basic;file=<file>;realm=<realm>;refresh=<interval>

Expand Down
1 change: 1 addition & 0 deletions docs/content/ref/proxy.auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The following types of authorization schemes are available:
The basic authorization scheme leverages [Http Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) and reads a [htpasswd](https://httpd.apache.org/docs/2.4/misc/password_encryptions.html) file at startup and credentials are cached until the service exits.

The `file` option contains the path to the htpasswd file. The `realm` parameter is optional (default is to use the `name`). The `refresh` option can set the htpasswd file refresh interval. Minimal refresh interval is `1s` to void busy loop. By default refresh is disabled i.e. set to zero.
Note: removing the htpasswd file will cause all requests to fail with HTTP status code 401 (Unauthorized) until the file is restored.

name=<name>;type=basic;file=<file>;realm=<realm>;refresh=<interval>

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ require (
github.com/sergi/go-diff v0.0.0-20170118131230-24e2351369ec
github.com/sirupsen/logrus v1.2.0 // indirect
github.com/soheilhy/cmux v0.1.4 // indirect
github.com/tg123/go-htpasswd v0.0.0-20181019180120-0849ceac46bc
github.com/tg123/go-htpasswd v0.0.0-20190305225429-d38e564730bf
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tg123/go-htpasswd v0.0.0-20181019180120-0849ceac46bc h1:xex1qjYrr0ez7t6pWHwYJrdKrJYzsvPQfoHG87u7FTs=
github.com/tg123/go-htpasswd v0.0.0-20181019180120-0849ceac46bc/go.mod h1:rFFqmvZbM9kVmDDVpeVr8VJ+8nRWwuXR/uvvjJ/3aJo=
github.com/tg123/go-htpasswd v0.0.0-20190305225429-d38e564730bf h1:lemWpSGw+Yz0k0lbnwoJXO5ovdxaS6uR7u3JTbPczRE=
github.com/tg123/go-htpasswd v0.0.0-20190305225429-d38e564730bf/go.mod h1:rFFqmvZbM9kVmDDVpeVr8VJ+8nRWwuXR/uvvjJ/3aJo=
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 h1:lYIiVDtZnyTWlNwiAxLj0bbpTcx1BWCFhXjfsvmPdNc=
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8=
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func newHTTPProxy(cfg *config.Config) http.Handler {
authSchemes, err := auth.LoadAuthSchemes(cfg.Proxy.AuthSchemes)

if err != nil {
exit.Fatal("[FATAL]", err)
exit.Fatal("[FATAL] ", err)
}

return &proxy.HTTPProxy{
Expand Down
5 changes: 2 additions & 3 deletions vendor/github.com/gogo/protobuf/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion vendor/github.com/gogo/protobuf/proto/decode.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions vendor/github.com/gogo/protobuf/proto/deprecated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 0 additions & 18 deletions vendor/github.com/gogo/protobuf/proto/encode.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vendor/github.com/gogo/protobuf/proto/extensions.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading