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

feat(web): basic auth #140

Merged
merged 1 commit into from
Aug 10, 2022
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
17 changes: 12 additions & 5 deletions config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,18 @@ type DataSettings struct {

// WebSettings for the binary.
type WebSettings struct {
ListenHost *string `yaml:"listen_host,omitempty"` // Web listen host
ListenPort *string `yaml:"listen_port,omitempty"` // Web listen port
RoutePrefix *string `yaml:"route_prefix,omitempty"` // Web endpoint prefix
CertFile *string `yaml:"cert_file,omitempty"` // HTTPS certificate path
KeyFile *string `yaml:"pkey_file,omitempty"` // HTTPS privkey path
ListenHost *string `yaml:"listen_host,omitempty"` // Web listen host
ListenPort *string `yaml:"listen_port,omitempty"` // Web listen port
RoutePrefix *string `yaml:"route_prefix,omitempty"` // Web endpoint prefix
CertFile *string `yaml:"cert_file,omitempty"` // HTTPS certificate path
KeyFile *string `yaml:"pkey_file,omitempty"` // HTTPS privkey path
BasicAuth *WebSettingsBasicAuth `yaml:"basic_auth,omitempty"` // Basic auth creds
}

// WebSettingsBasicAuth contains the basic auth credentials to use (if any)
type WebSettingsBasicAuth struct {
Username string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
}

func (s *Settings) NilUndefinedFlags(flagset *map[string]bool) {
Expand Down
3 changes: 3 additions & 0 deletions web/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ func NewAPI(cfg *config.Config, log *utils.JLog) *API {
RoutePrefix: routePrefix,
}
baseRouter.Handle(routePrefix, http.RedirectHandler(routePrefix+"/", http.StatusPermanentRedirect))
if api.Config.Settings.Web.BasicAuth != nil {
api.Router.Use(api.basicAuth())
}
return api
}
30 changes: 30 additions & 0 deletions web/api/v1/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,48 @@
package v1

import (
"crypto/sha256"
"crypto/subtle"
"encoding/json"
"io/fs"
"net/http"
"strings"

"github.com/gorilla/mux"
"github.com/release-argus/Argus/utils"
api_types "github.com/release-argus/Argus/web/api/types"
"github.com/release-argus/Argus/web/ui"
"github.com/vearutop/statigz"
"github.com/vearutop/statigz/brotli"
)

func (api *API) basicAuth() mux.MiddlewareFunc {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if ok {
// Hash purely to prevent ConstantTimeCompare leaking lengths
usernameHash := sha256.Sum256([]byte(username))
passwordHash := sha256.Sum256([]byte(password))
expectedUsernameHash := sha256.Sum256([]byte(api.Config.Settings.Web.BasicAuth.Username))
expectedPasswordHash := sha256.Sum256([]byte(api.Config.Settings.Web.BasicAuth.Password))

// Protect from possible timing attacks
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)

if usernameMatch && passwordMatch {
h.ServeHTTP(w, r)
return
}
}

w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
})
}
}

// SetupRoutesAPI will setup the HTTP API routes.
func (api *API) SetupRoutesAPI() {
api.Router.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
Expand Down
57 changes: 57 additions & 0 deletions web/api/v1/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net/http/httptest"
"testing"

"github.com/release-argus/Argus/config"
"github.com/release-argus/Argus/utils"
api_types "github.com/release-argus/Argus/web/api/types"
)
Expand Down Expand Up @@ -59,3 +60,59 @@ func TestHTTPVersion(t *testing.T) {
want, got)
}
}

func TestBasicAuth(t *testing.T) {
// GIVEN an API with/without Basic Auth credentials
tests := map[string]struct {
basicAuth *config.WebSettingsBasicAuth
fail bool
noHeader bool
}{
"No basic auth": {basicAuth: nil, fail: false},
"basic auth fail invalid creds": {basicAuth: &config.WebSettingsBasicAuth{Username: "test", Password: "1234"}, fail: true},
"basic auth fail no Authorization header": {basicAuth: &config.WebSettingsBasicAuth{Username: "test", Password: "1234"}, noHeader: true, fail: true},
"basic auth pass": {basicAuth: &config.WebSettingsBasicAuth{Username: "test", Password: "123"}, fail: false},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
cfg := config.Config{}
cfg.Settings.Web.BasicAuth = tc.basicAuth
cfg.Settings.Web.RoutePrefix = stringPtr("")
api := NewAPI(&cfg, utils.NewJLog("WARN", false))
api.Router.HandleFunc("/test", func(rw http.ResponseWriter, req *http.Request) {
return
})
ts := httptest.NewServer(api.BaseRouter)
defer ts.Close()

// WHEN a HTTP request is made to this router
client := http.Client{}
req, err := http.NewRequest("GET", ts.URL+"/test", nil)
if err != nil {
t.Fatal(err)
}
if !tc.noHeader {
req.Header = http.Header{
// test:123
"Authorization": {"Basic dGVzdDoxMjM="},
}
}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}

// THEN the request passes only when expected
got := resp.StatusCode
want := 200
if tc.fail {
want = http.StatusUnauthorized
}
if got != want {
t.Errorf("Expected a %d, not a %d",
want, got)
}
})
}
}