├── .travis.yml ├── env_test.go ├── env.go ├── README.md ├── LICENSE ├── basicauth.go └── basicauth_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | - 1.5 6 | - 1.6 7 | 8 | install: 9 | - go get github.com/stretchr/testify/assert -------------------------------------------------------------------------------- /env_test.go: -------------------------------------------------------------------------------- 1 | package basicauth 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestEnvLoading(t *testing.T) { 13 | os.Setenv("TESTAPI_BOB", "bobspassword") 14 | 15 | called := false 16 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | called = true 18 | }) 19 | 20 | h := NewFromEnv("testrealm", "TESTAPI_")(next) 21 | 22 | w := &httptest.ResponseRecorder{} 23 | r, _ := http.NewRequest("GET", "/", nil) 24 | r.SetBasicAuth("bob", "bobspassword") 25 | h.ServeHTTP(w, r) 26 | 27 | assert.Equal(t, true, called) 28 | assertNotDenied(t, w) 29 | } 30 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | package basicauth 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | // NewFromEnv reads a set of credentials in from environment variables in 12 | // the format {PREFIX}{USERNAME|tolower}=password1,password2 and returns 13 | // middleware that will validate incoming requests. 14 | func NewFromEnv(realm, prefix string) func(http.Handler) http.Handler { 15 | credentials := map[string][]string{} 16 | 17 | re := regexp.MustCompile(fmt.Sprintf("^%s(?P.*)$", strings.ToUpper(prefix))) 18 | for _, envVar := range os.Environ() { 19 | name, value := split2(envVar, "=") 20 | 21 | if res := re.FindStringSubmatch(name); res != nil { 22 | username := strings.ToLower(res[1]) 23 | credentials[username] = strings.Split(value, ",") 24 | } 25 | } 26 | 27 | return New(realm, credentials) 28 | } 29 | 30 | func split2(s, sep string) (string, string) { 31 | res := strings.SplitN(s, sep, 2) 32 | 33 | return res[0], res[1] 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | basicauth-go 2 | ================= 3 | [![GoDoc](https://godoc.org/github.com/99designs/basicauth-go?status.svg)](https://godoc.org/github.com/99designs/basicauth-go) 4 | [![Build Status](https://travis-ci.org/99designs/basicauth-go.svg)](https://travis-ci.org/99designs/basicauth-go) 5 | 6 | 7 | golang middleware for HTTP basic auth. 8 | 9 | ```go 10 | // Chi 11 | 12 | router.Use(basicauth.New("MyRealm", map[string][]string{ 13 | "bob": {"password1", "password2"}, 14 | })) 15 | 16 | 17 | // Manual wrapping 18 | 19 | middleware := basicauth.New("MyRealm", map[string][]string{ 20 | "bob": {"password1", "password2"}, 21 | }) 22 | 23 | h := middlware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)) { 24 | /// do stuff 25 | }) 26 | 27 | log.Fatal(http.ListenAndServe(":8080", h)) 28 | ``` 29 | 30 | ### env loading 31 | If your environment looks like this: 32 | ```bash 33 | SOME_PREFIX_BOB=password 34 | SOME_PREFIX_JANE=password1,password2 35 | ``` 36 | 37 | you can load it like this: 38 | ```go 39 | middleware := basicauth.NewFromEnv("MyRealm", "SOME_PREFIX") 40 | ``` 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 99designs 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /basicauth.go: -------------------------------------------------------------------------------- 1 | package basicauth 2 | 3 | import ( 4 | "crypto/subtle" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // New returns a piece of middleware that will allow access only 10 | // if the provided credentials match within the given service 11 | // otherwise it will return a 401 and not call the next handler. 12 | func New(realm string, credentials map[string][]string) func(http.Handler) http.Handler { 13 | return func(next http.Handler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | username, password, ok := r.BasicAuth() 16 | if !ok { 17 | unauthorized(w, realm) 18 | return 19 | } 20 | 21 | validPasswords, userFound := credentials[username] 22 | if !userFound { 23 | unauthorized(w, realm) 24 | return 25 | } 26 | 27 | for _, validPassword := range validPasswords { 28 | validPasswordBytes := []byte(validPassword) 29 | passwordBytes := []byte(password) 30 | // take the same amount of time if the lengths are different 31 | // this is required since ConstantTimeCompare returns immediately when slices of different length are compared 32 | if len(password) != len(validPassword) { 33 | subtle.ConstantTimeCompare(validPasswordBytes, validPasswordBytes) 34 | } else { 35 | if subtle.ConstantTimeCompare(passwordBytes, validPasswordBytes) == 1 { 36 | next.ServeHTTP(w, r) 37 | return 38 | } 39 | } 40 | } 41 | 42 | unauthorized(w, realm) 43 | }) 44 | } 45 | } 46 | 47 | func unauthorized(w http.ResponseWriter, realm string) { 48 | w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm)) 49 | w.WriteHeader(http.StatusUnauthorized) 50 | } 51 | -------------------------------------------------------------------------------- /basicauth_test.go: -------------------------------------------------------------------------------- 1 | package basicauth 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNoAuthGetsDenied(t *testing.T) { 12 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | t.Error("Should not call handler") 14 | }) 15 | 16 | h := New("testrealm", map[string][]string{})(next) 17 | 18 | w := &httptest.ResponseRecorder{} 19 | r, _ := http.NewRequest("GET", "/", nil) 20 | h.ServeHTTP(w, r) 21 | 22 | assertDenied(t, w) 23 | } 24 | 25 | func TestCorrectCredentialsGetsAllowed(t *testing.T) { 26 | called := false 27 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | called = true 29 | }) 30 | 31 | h := New("testrealm", map[string][]string{ 32 | "bob": {"bobspassword"}, 33 | })(next) 34 | 35 | w := &httptest.ResponseRecorder{} 36 | r, _ := http.NewRequest("GET", "/", nil) 37 | r.SetBasicAuth("bob", "bobspassword") 38 | h.ServeHTTP(w, r) 39 | 40 | assert.Equal(t, true, called) 41 | assertNotDenied(t, w) 42 | } 43 | 44 | func TestInvalidPasswordIsDeined(t *testing.T) { 45 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 | t.Error("Should not call handler") 47 | }) 48 | 49 | h := New("testrealm", map[string][]string{ 50 | "bob": {"bobspassword"}, 51 | })(next) 52 | 53 | w := &httptest.ResponseRecorder{} 54 | r, _ := http.NewRequest("GET", "/", nil) 55 | r.SetBasicAuth("bob", "notbobspassword") 56 | h.ServeHTTP(w, r) 57 | 58 | assertDenied(t, w) 59 | } 60 | 61 | func TestInvalidUserIsDenied(t *testing.T) { 62 | next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 63 | t.Error("Should not call handler") 64 | }) 65 | 66 | h := New("testrealm", map[string][]string{ 67 | "bob": {"bobspassword"}, 68 | })(next) 69 | 70 | w := &httptest.ResponseRecorder{} 71 | r, _ := http.NewRequest("GET", "/", nil) 72 | r.SetBasicAuth("jane", "bobspassword") 73 | h.ServeHTTP(w, r) 74 | 75 | assertDenied(t, w) 76 | } 77 | 78 | func assertNotDenied(t *testing.T, w *httptest.ResponseRecorder) { 79 | assert.NotEqual(t, http.StatusUnauthorized, w.Code) 80 | } 81 | 82 | func assertDenied(t *testing.T, w *httptest.ResponseRecorder) { 83 | assert.Equal(t, `Basic realm="testrealm"`, w.HeaderMap.Get("WWW-Authenticate")) 84 | assert.Equal(t, http.StatusUnauthorized, w.Code) 85 | } 86 | --------------------------------------------------------------------------------