diff --git a/README.md b/README.md index 1dcfdc7..071fe0d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # goji/httpauth [![GoDoc](https://godoc.org/github.com/goji/httpauth?status.svg)](https://godoc.org/github.com/goji/httpauth) [![Build Status](https://travis-ci.org/goji/httpauth.svg)](https://travis-ci.org/goji/httpauth) -httpauth currently provides [HTTP Basic Authentication middleware](http://tools.ietf.org/html/rfc2617) for Go. It is compatible with Go's own `net/http`, [goji](https://goji.io), Gin & anything that speaks the `http.Handler` interface. +`httpauth` currently provides [HTTP Basic Authentication middleware](http://tools.ietf.org/html/rfc2617) for Go. It is compatible with Go's own `net/http`, [goji](https://goji.io), Gin & anything that speaks the `http.Handler` interface. ## Example -httpauth provides a `SimpleBasicAuth` function to get you up and running. Particularly ideal for development servers. +`httpauth` provides a `SimpleBasicAuth` function to get you up and running. Particularly ideal for development servers. Note that HTTP Basic Authentication credentials are sent over the wire "in the clear" (read: plaintext!) and therefore should not be considered a robust way to secure a HTTP server. If you're after that, you'll need to use SSL/TLS ("HTTPS") at a minimum. @@ -16,6 +16,11 @@ $ go get github.com/goji/httpauth ### Goji v2 +#### Simple Usage + +The fastest and simplest way to get started using `httpauth` is to use the +`SimpleBasicAuth` function. + ```go package main @@ -39,10 +44,13 @@ func main() { } ``` -If you're looking for a little more control over the process, you can instead pass a `httpauth.AuthOptions` struct to `httpauth.BasicAuth` instead. This allows you to: +#### Advanced Usage -* Configure the authentication realm +For more control over the process, pass a `AuthOptions` struct to `BasicAuth` instead. This allows you to: + +* Configure the authentication realm. * Provide your own UnauthorizedHandler (anything that satisfies `http.Handler`) so you can return a better looking 401 page. +* Define a custom authentication function, which is discussed in the next section. ```go @@ -66,9 +74,42 @@ func main() { } ``` +#### Custom Authentication Function + +`httpauth` will accept a custom authentication function. +Normally, you would not set `AuthOptions.User` nor `AuthOptions.Password` in this scenario. +You would instead validate the given credentials against an external system such as a database. +The contrived example below is for demonstration purposes only. + +```go +func main() { + + authOpts := httpauth.AuthOptions{ + Realm: "DevCo", + AuthFunc: myAuthFunc, + UnauthorizedHandler: myUnauthorizedHandler, + } + + mux := goji.NewMux() + + mux.Use(BasicAuth(authOpts)) + mux.Use(SomeOtherMiddleware) + + mux.Handle(pat.Get("/some-route"), YourHandler)) + + log.Fatal(http.ListenAndServe("localhost:8000", mux)) +} + +// myAuthFunc is not secure. It checks to see if the password is simply +// the username repeated three times. +func myAuthFunc(user, pass string) bool { + return pass == strings.Repeat(user, 3) +} +``` + ### gorilla/mux -Since it's all `http.Handler`, httpauth works with gorilla/mux (and most other routers) as well: +Since it's all `http.Handler`, `httpauth` works with [gorilla/mux](https://github.com/gorilla/mux) (and most other routers) as well: ```go package main @@ -96,7 +137,7 @@ func YourHandler(w http.ResponseWriter, r *http.Request) { ### net/http -If you're using vanilla net/http: +If you're using vanilla `net/http`: ```go package main @@ -115,7 +156,7 @@ func main() { ## Contributing -Send a pull request! Note that features on the (informal) roadmap include HTTP Digest Auth and the potential for supplying your own user/password comparison function. +Send a pull request! Note that features on the (informal) roadmap include HTTP Digest Auth. ## License diff --git a/basic_auth.go b/basic_auth.go index caffafa..6469212 100644 --- a/basic_auth.go +++ b/basic_auth.go @@ -23,6 +23,7 @@ type AuthOptions struct { Realm string User string Password string + AuthFunc func(string, string) bool UnauthorizedHandler http.Handler } @@ -48,8 +49,13 @@ func (b basicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (b *basicAuth) authenticate(r *http.Request) bool { const basicScheme string = "Basic " - // Prevent authentication with empty credentials if User and Password is not set - if b.opts.User == "" || b.opts.Password == "" { + if r == nil { + return false + } + + // In simple mode, prevent authentication with empty credentials if User or + // Password is not set. + if b.opts.AuthFunc == nil && (b.opts.User == "" || b.opts.Password == "") { return false } @@ -59,7 +65,7 @@ func (b *basicAuth) authenticate(r *http.Request) bool { return false } - // Get the plain-text username and password from the request + // Get the plain-text username and password from the request. // The first six characters are skipped - e.g. "Basic ". str, err := base64.StdEncoding.DecodeString(auth[len(basicScheme):]) if err != nil { @@ -75,10 +81,24 @@ func (b *basicAuth) authenticate(r *http.Request) bool { return false } + givenUser := string(creds[0]) + givenPass := string(creds[1]) + + // Default to Simple mode if no AuthFunc is defined. + if b.opts.AuthFunc == nil { + b.opts.AuthFunc = b.simpleBasicAuthFunc + } + + return b.opts.AuthFunc(givenUser, givenPass) +} + +// simpleBasicAuthFunc authenticates the supplied username and password against +// the User and Password set in the Options struct. +func (b *basicAuth) simpleBasicAuthFunc(user, pass string) bool { // Equalize lengths of supplied and required credentials // by hashing them - givenUser := sha256.Sum256(creds[0]) - givenPass := sha256.Sum256(creds[1]) + givenUser := sha256.Sum256([]byte(user)) + givenPass := sha256.Sum256([]byte(pass)) requiredUser := sha256.Sum256([]byte(b.opts.User)) requiredPass := sha256.Sum256([]byte(b.opts.Password)) @@ -113,7 +133,7 @@ func defaultUnauthorizedHandler(w http.ResponseWriter, r *http.Request) { // import( // "net/http" // "github.com/zenazn/goji" -// "github.com/zenazn/goji/web/httpauth" +// "github.com/goji/httpauth" // ) // // func main() { diff --git a/basic_auth_test.go b/basic_auth_test.go index 4dcd5e0..1b598fe 100644 --- a/basic_auth_test.go +++ b/basic_auth_test.go @@ -6,6 +6,58 @@ import ( "testing" ) +func TestBasicAuthAuthenticateWithFunc(t *testing.T) { + requiredUser := "jqpublic" + requiredPass := "secret.sauce" + + // Dumb test function + fn := func(u, p string) bool { + return u == requiredUser && p == requiredPass + } + + // Provide a minimal test implementation. + authOpts := AuthOptions{ + Realm: "Restricted", + AuthFunc: fn, + } + + b := &basicAuth{opts: authOpts} + + r := &http.Request{Method: "GET"} + + if b.authenticate(nil) { + t.Fatal("Should not succeed when http.Request is nil") + } + + // Provide auth data, but no Authorization header + if b.authenticate(r) != false { + t.Fatal("No Authorization header supplied.") + } + + // Initialise the map for HTTP headers + r.Header = http.Header(make(map[string][]string)) + + // Set a malformed/bad header + r.Header.Set("Authorization", " Basic") + if b.authenticate(r) != false { + t.Fatal("Malformed Authorization header supplied.") + } + + // Test correct credentials + auth := base64.StdEncoding.EncodeToString([]byte("jqpublic:secret.sauce")) + r.Header.Set("Authorization", "Basic "+auth) + if b.authenticate(r) != true { + t.Fatal("Failed on correct credentials") + } + + // Test incorrect credentials + auth = base64.StdEncoding.EncodeToString([]byte("jqpublic:hackydoo")) + r.Header.Set("Authorization", "Basic "+auth) + if b.authenticate(r) == true { + t.Fatal("Success when expecting failure") + } +} + func TestBasicAuthAuthenticate(t *testing.T) { // Provide a minimal test implementation. authOpts := AuthOptions{ @@ -18,8 +70,7 @@ func TestBasicAuthAuthenticate(t *testing.T) { opts: authOpts, } - r := &http.Request{} - r.Method = "GET" + r := &http.Request{Method: "GET"} // Provide auth data, but no Authorization header if b.authenticate(r) != false { @@ -43,10 +94,23 @@ func TestBasicAuthAuthenticate(t *testing.T) { } } -func TestBasicAuthAutenticateWithouUserAndPass(t *testing.T) { +func TestBasicAuthAuthenticateWithoutUserAndPass(t *testing.T) { b := basicAuth{opts: AuthOptions{}} - if b.authenticate(nil) { - t.Fatal("Should not authenticate if user or pass are not set on opts") + r := &http.Request{Method: "GET"} + + // Provide auth data, but no Authorization header + if b.authenticate(r) != false { + t.Fatal("No Authorization header supplied.") + } + + // Initialise the map for HTTP headers + r.Header = http.Header(make(map[string][]string)) + + // Test correct credentials + auth := base64.StdEncoding.EncodeToString([]byte(b.opts.User + ":" + b.opts.Password)) + r.Header.Set("Authorization", "Basic "+auth) + if b.authenticate(r) != false { + t.Fatal("Success when expecting failure") } }