├── LICENSE ├── README.md ├── basic.go ├── basic_test.go ├── util.go ├── util_test.go └── wercker.yml /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jeremy Saenz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # auth [![wercker status](https://app.wercker.com/status/8e5237b01b52f169a1274fad9a89617b "wercker status")](https://app.wercker.com/project/bykey/8e5237b01b52f169a1274fad9a89617b) 2 | Martini middleware/handler for http basic authentication. 3 | 4 | [API Reference](http://godoc.org/github.com/martini-contrib/auth) 5 | 6 | ## Simple Usage 7 | 8 | Use `auth.Basic` to authenticate against a pre-defined username and password: 9 | 10 | ~~~ go 11 | import ( 12 | "github.com/go-martini/martini" 13 | "github.com/martini-contrib/auth" 14 | ) 15 | 16 | func main() { 17 | m := martini.Classic() 18 | // authenticate every request 19 | m.Use(auth.Basic("username", "secretpassword")) 20 | m.Run() 21 | } 22 | ~~~ 23 | 24 | ## Advanced Usage 25 | 26 | Using `auth.BasicFunc` lets you authenticate on a per-user level, by checking 27 | the username and password in the callback function: 28 | 29 | ~~~ go 30 | import ( 31 | "github.com/go-martini/martini" 32 | "github.com/martini-contrib/auth" 33 | ) 34 | 35 | func main() { 36 | m := martini.Classic() 37 | // authenticate every request 38 | m.Use(auth.BasicFunc(func(username, password string) bool { 39 | return username == "admin" && password == "guessme" 40 | })) 41 | m.Run() 42 | } 43 | ~~~ 44 | 45 | Note that checking usernames and passwords with string comparison might be 46 | susceptible to timing attacks. To avoid that, use `auth.SecureCompare` instead: 47 | 48 | ~~~ go 49 | m.Use(auth.BasicFunc(func(username, password string) bool { 50 | return auth.SecureCompare(username, "admin") && auth.SecureCompare(password, "guessme") 51 | })) 52 | } 53 | ~~~ 54 | 55 | Upon successful authentication, the username is available to all subsequent 56 | handlers via the `auth.User` type: 57 | 58 | ~~~ go 59 | m.Get("/", func(user auth.User) string { 60 | return "Welcome, " + string(user) 61 | }) 62 | } 63 | ~~~ 64 | 65 | ## Authors 66 | * [Jeremy Saenz](http://github.com/codegangsta) 67 | * [Brendon Murphy](http://github.com/bemurphy) 68 | -------------------------------------------------------------------------------- /basic.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/go-martini/martini" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | // User is the authenticated username that was extracted from the request. 11 | type User string 12 | 13 | // BasicRealm is used when setting the WWW-Authenticate response header. 14 | var BasicRealm = "Authorization Required" 15 | 16 | // Basic returns a Handler that authenticates via Basic Auth. Writes a http.StatusUnauthorized 17 | // if authentication fails. 18 | func Basic(username string, password string) martini.Handler { 19 | var siteAuth = base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) 20 | return func(res http.ResponseWriter, req *http.Request, c martini.Context) { 21 | auth := req.Header.Get("Authorization") 22 | if !SecureCompare(auth, "Basic "+siteAuth) { 23 | unauthorized(res) 24 | return 25 | } 26 | c.Map(User(username)) 27 | } 28 | } 29 | 30 | // BasicFunc returns a Handler that authenticates via Basic Auth using the provided function. 31 | // The function should return true for a valid username/password combination. 32 | func BasicFunc(authfn func(string, string) bool) martini.Handler { 33 | return func(res http.ResponseWriter, req *http.Request, c martini.Context) { 34 | auth := req.Header.Get("Authorization") 35 | if len(auth) < 6 || auth[:6] != "Basic " { 36 | unauthorized(res) 37 | return 38 | } 39 | b, err := base64.StdEncoding.DecodeString(auth[6:]) 40 | if err != nil { 41 | unauthorized(res) 42 | return 43 | } 44 | tokens := strings.SplitN(string(b), ":", 2) 45 | if len(tokens) != 2 || !authfn(tokens[0], tokens[1]) { 46 | unauthorized(res) 47 | return 48 | } 49 | c.Map(User(tokens[0])) 50 | } 51 | } 52 | 53 | func unauthorized(res http.ResponseWriter) { 54 | res.Header().Set("WWW-Authenticate", "Basic realm=\""+BasicRealm+"\"") 55 | http.Error(res, "Not Authorized", http.StatusUnauthorized) 56 | } 57 | -------------------------------------------------------------------------------- /basic_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/go-martini/martini" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func Test_BasicAuth(t *testing.T) { 12 | recorder := httptest.NewRecorder() 13 | 14 | auth := "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")) 15 | 16 | m := martini.New() 17 | m.Use(Basic("foo", "bar")) 18 | m.Use(func(res http.ResponseWriter, req *http.Request, u User) { 19 | res.Write([]byte("hello " + u)) 20 | }) 21 | 22 | r, _ := http.NewRequest("GET", "foo", nil) 23 | 24 | m.ServeHTTP(recorder, r) 25 | 26 | if recorder.Code != 401 { 27 | t.Error("Response not 401") 28 | } 29 | 30 | if recorder.Body.String() == "hello foo" { 31 | t.Error("Auth block failed") 32 | } 33 | 34 | recorder = httptest.NewRecorder() 35 | r.Header.Set("Authorization", auth) 36 | m.ServeHTTP(recorder, r) 37 | 38 | if recorder.Code == 401 { 39 | t.Error("Response is 401") 40 | } 41 | 42 | if recorder.Body.String() != "hello foo" { 43 | t.Error("Auth failed, got: ", recorder.Body.String()) 44 | } 45 | } 46 | 47 | func Test_BasicFuncAuth(t *testing.T) { 48 | for auth, valid := range map[string]bool{ 49 | "foo:spam": true, 50 | "bar:spam": true, 51 | "foo:eggs": false, 52 | "bar:eggs": false, 53 | "baz:spam": false, 54 | "foo:spam:extra": false, 55 | "dummy:": false, 56 | "dummy": false, 57 | "": false, 58 | } { 59 | recorder := httptest.NewRecorder() 60 | encoded := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) 61 | 62 | m := martini.New() 63 | m.Use(BasicFunc(func(username, password string) bool { 64 | return (username == "foo" || username == "bar") && password == "spam" 65 | })) 66 | m.Use(func(res http.ResponseWriter, req *http.Request) { 67 | res.Write([]byte("hello")) 68 | }) 69 | 70 | r, _ := http.NewRequest("GET", "foo", nil) 71 | 72 | m.ServeHTTP(recorder, r) 73 | 74 | if recorder.Code != 401 { 75 | t.Error("Response not 401, params:", auth) 76 | } 77 | 78 | if recorder.Body.String() == "hello" { 79 | t.Error("Auth block failed, params:", auth) 80 | } 81 | 82 | recorder = httptest.NewRecorder() 83 | r.Header.Set("Authorization", encoded) 84 | m.ServeHTTP(recorder, r) 85 | 86 | if valid && recorder.Code == 401 { 87 | t.Error("Response is 401, params:", auth) 88 | } 89 | if !valid && recorder.Code != 401 { 90 | t.Error("Response not 401, params:", auth) 91 | } 92 | 93 | if valid && recorder.Body.String() != "hello" { 94 | t.Error("Auth failed, got: ", recorder.Body.String(), "params:", auth) 95 | } 96 | if !valid && recorder.Body.String() == "hello" { 97 | t.Error("Auth block failed, params:", auth) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/sha256" 5 | "crypto/subtle" 6 | ) 7 | 8 | // SecureCompare performs a constant time compare of two strings to limit timing attacks. 9 | func SecureCompare(given string, actual string) bool { 10 | givenSha := sha256.Sum256([]byte(given)) 11 | actualSha := sha256.Sum256([]byte(actual)) 12 | 13 | return subtle.ConstantTimeCompare(givenSha[:], actualSha[:]) == 1 14 | } 15 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var comparetests = []struct { 8 | a string 9 | b string 10 | val bool 11 | }{ 12 | {"foo", "foo", true}, 13 | {"bar", "bar", true}, 14 | {"password", "password", true}, 15 | {"Foo", "foo", false}, 16 | {"foo", "foobar", false}, 17 | {"password", "pass", false}, 18 | } 19 | 20 | func Test_SecureCompare(t *testing.T) { 21 | for _, tt := range comparetests { 22 | if SecureCompare(tt.a, tt.b) != tt.val { 23 | t.Errorf("Expected SecureCompare(%v, %v) to return %v but did not", tt.a, tt.b, tt.val) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: wercker/golang@1.1.1 --------------------------------------------------------------------------------