41
votes

Situation:

I'm building a REST API using Gorilla's mux as the router.

I'm wondering how I can protect specific routes with simple HTTP Basic Auth. I don't have a need to read the credentials from a file or any external source, I really just want to protect selected routes by a hard coded HTTP Basic Auth username and password.

Question:

What is the idiomatic way of doing so in Go? Does Gorilla offer anything to make it more easy? If you could provide a few lines of code, that would be just wonderful.

6
You're gonna want to make a middleware that checks the Authorization header. github.com/gorilla/mux/pull/36Dustin Kingen

6 Answers

45
votes

Combining a couple of answers into an easy copy/paste:

// BasicAuth wraps a handler requiring HTTP basic auth for it using the given
// username and password and the specified realm, which shouldn't contain quotes.
//
// Most web browser display a dialog with something like:
//
//    The website says: "<realm>"
//
// Which is really stupid so you may want to set the realm to a message rather than
// an actual realm.
func BasicAuth(handler http.HandlerFunc, username, password, realm string) http.HandlerFunc {

    return func(w http.ResponseWriter, r *http.Request) {

        user, pass, ok := r.BasicAuth()

        if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
            w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
            w.WriteHeader(401)
            w.Write([]byte("Unauthorised.\n"))
            return
        }

        handler(w, r)
    }
}

...

http.HandleFunc("/", BasicAuth(handleIndex, "admin", "123456", "Please enter your username and password for this site"))

Note that subtle.ConstantTimeCompare() still depends on the length, so it is probably possible for attackers to work out the length of the username and password if you do it like this. To get around that you could hash them or add a fixed delay.

24
votes

Check req.BasicAuth() https://golang.org/pkg/net/http/#Request.BasicAuth

You can check this in your handler or wrap your handler like so:

func auth(fn http.HandlerFunc) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    user, pass, _ := r.BasicAuth()
    if !check(user, pass) {
       http.Error(w, "Unauthorized.", 401)
       return
    }
    fn(w, r)
  }
}

Where

check(u, p string) bool 

is a function you will have to write yourself based on how you are storing credentials. Now you can use:

auth(originalHandler)

Wherever you were passing originalHandler before.

[edit: It's worth adding that your check function should be resistant to side channel attacks like timing attacks. Also stored passwords should be hashed with a cryptographically random salt. Also you should probably use OAuth instead and let an established identity provider worry about password security for you.]

20
votes

As of 2016, I would suggest to use this answer. In any case, wrap your HTTP basic auth in SSL to avoid sending username and password as plain text.


Just wrap your handler in another handler and use issue WWW-Authorization header on the incoming request.

Example (full version):

func checkAuth(w http.ResponseWriter, r *http.Request) bool {
    s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
    if len(s) != 2 { return false }

    b, err := base64.StdEncoding.DecodeString(s[1])
    if err != nil { return false }

    pair := strings.SplitN(string(b), ":", 2)
    if len(pair) != 2 { return false }

    return pair[0] == "user" && pair[1] == "pass"
}

yourRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    if checkAuth(w, r) {
        yourOriginalHandler.ServeHTTP(w, r)
        return
    }

    w.Header().Set("WWW-Authenticate", `Basic realm="MY REALM"`)
    w.WriteHeader(401)
    w.Write([]byte("401 Unauthorized\n"))
})

Unfortunately, the std. library only offers client basic auth and therefore you have to do it yourself or use a library, for example this one.

4
votes

The net/http request type has helper functions for doing this (tested on go 1.7). A simpler version of nemo's answer would look like such:

func basicAuthHandler(user, pass, realm string, next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if checkBasicAuth(r, user, pass) {
            next(w, r)
            return
        }

        w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
        w.WriteHeader(401)
        w.Write([]byte("401 Unauthorized\n"))
    }
}

func checkBasicAuth(r *http.Request, user, pass string) bool {
    u, p, ok := r.BasicAuth()
    if !ok {
        return false
    }
    return u == user && p == pass
}

then simply create your handler with business logic and pass it as the next argument in basicAuthHandler to create a new "wrapped" handlerFunc.

3
votes

go-http-auth will do it for you. It will fit right in if you're using net/http.

1
votes

I realize I'm late to the party here. I just happened to be revisiting HTTP Basic Authentication. After going through all the answers here I actually settled on a variation of Timmmm's solution. I even took it the step farther and added the hashing as per his suggestion to improve security. Figured I might as well share my variation of the code.

userhash := hasher("admin")
passhash := hasher("$CrazyUnforgettablePassword?")
realm := "Please enter username and password"

http.HandleFunc("/", authHandler(indexHandler, userhash, passhash, realm))

// Above code should obviously be in main() along with the http listener, etc.

// hasher uses package "crypto/sha256"
func hasher(s string) []byte {
    val := sha256.Sum256([]byte(s))
    return val[:]
}

func authHandler(handler http.HandlerFunc, userhash, passhash []byte, realm string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        user, pass, ok := r.BasicAuth()
        if !ok || subtle.ConstantTimeCompare(hasher(user),
            userhash) != 1 || subtle.ConstantTimeCompare(hasher(pass), passhash) != 1 {
            w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
            http.Error(w, "Unauthorized.", http.StatusUnauthorized)
            return
        }
        handler(w, r)
    }
}