Skip to content

Commit

Permalink
Add custom authentication function feature
Browse files Browse the repository at this point in the history
This commit adds a new AuthFunc field to the AuthOptions struct.  The User and
Password fields are left in-place for backwards compatibility.  This construct
shouldn't break any user code unless they are using unlabeled struct literals,
in which case it should break on principle since you shouldn't do that on
structs outside of your control.
  • Loading branch information
moorereason committed Dec 23, 2015
1 parent ae097d1 commit 723e9a2
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 18 deletions.
55 changes: 48 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
32 changes: 26 additions & 6 deletions basic_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type AuthOptions struct {
Realm string
User string
Password string
AuthFunc func(string, string) bool
UnauthorizedHandler http.Handler
}

Expand All @@ -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
}

Expand All @@ -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 {
Expand All @@ -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))

Expand Down Expand Up @@ -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() {
Expand Down
74 changes: 69 additions & 5 deletions basic_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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 {
Expand All @@ -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")
}
}

3 comments on commit 723e9a2

@gaffneyc
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recently updated the version of httpauth in an application and this commit did break our application as it no longer allows an empty password. It's a somewhat common practice to use an API key as the username with an empty password.

@elithrar
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gaffneyc I'm going to revert this change, as the API key over Basic Auth is an approach that should be supported. Fixed in #18 and now back in master as of 2da839a.

@gaffneyc
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elithrar Thank you for the quick response! It's really appreciated.

Please sign in to comment.