Not sure how to structure your Go web application? My book Let's Go is a clear, concise and easy-to-follow guide which packs in everything you need to know about best practices, project structure and practical code patterns. Take a look!

Published on:
Filed under: golang tutorial

How to correctly use Basic Authentication in Go

When searching for examples of HTTP basic authentication with Go, every result I could find unfortunately contained code which is either out-of-date (i.e. doesn't use the r.BasicAuth() functionality that was introduced in Go 1.4) or doesn't protect against timing attacks.

So in this post I'd like to quickly discuss how to implement it correctly in your Go applications.

We'll start with a bit of background information, but if you're not interested in that you can skip straight to the code.

What is basic authentication? When should I use it?

As a developer, you're probably already familiar with the prompt that web browsers show when you visit a protected URL.

When you input a username and password into this prompt, the web browser will send a HTTP request to the server containing an Authorization header — similar to this:

Authorization: Basic YWxpY2U6cGE1NXdvcmQ=

The Authorization header value is made up of the string Basic followed by the username and password in the format username:password and base-64 encoded. In this specific example, YWxpY2U6cGE1NXdvcmQ= is the base-64 encoding of the value alice:pa55word.

When the server receives this request, it can decode the username and password from the Authorization header and check that they are valid. If the credentials are not valid, the server can return a 401 Unauthorized response and the browser can redisplay the prompt.

Basic authentication can be used in lots of different scenarios, but it's often a good fit for when you have a low-value resource and want a quick and easy way to protect it from prying eyes.

To help keep things secure you should:

  • Only ever use it over HTTPS connections. If you don't use HTTPS, the Authorization header can potentially be intercepted and decoded by an attacker, who can then use the username and password to gain access to your protected resources.

  • Use a strong password that is difficult for attackers to guess or brute-force.

  • Consider adding rate limiting to your application, to make it harder for an attacker to brute-force the credentials.

It's also worth pointing out that basic auth is supported out-of-the-box by most programming languages and command-line tools such as curl and wget, as well as web browsers.

Protecting a web application

The simplest way to protect your application is to create some middleware. In this middleware we want to do three things:

  • Extract the username and password from the request Authorization header, if it exists. The best way to do this is with the r.BasicAuth() method which was introduced in Go 1.4.

  • Compare the provided username and password against the values that you expect. To avoid the risk of timing attacks, you should use Go's subtle.ConstantTimeCompare() function to do this comparison.

    It's also important to be aware that using subtle.ConstantTimeCompare() can leak information about username and password length. To prevent this, we should hash both the provided and expected username and password values using a fast cryptographic hash function like SHA-256 before comparing them. This ensures that both the provided and expected values that we are comparing are equal in length and prevents subtle.ConstantTimeCompare() itself from returning early.

  • If the username and password are not correct, or the request didn't contain a valid Authorization header, then the middleware should send a 401 Unauthorized response and set a WWW-Authenticate header to inform to the client that basic authentication should be used to gain access. Otherwise, the middleware should allow the request to proceed and call the next handler in the chain.

Putting that together, the pattern for implementing some middleware looks like this:

func basicAuth(next http.HandlerFunc) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Extract the username and password from the request 
        // Authorization header. If no Authentication header is present 
        // or the header value is invalid, then the 'ok' return value 
        // will be false.
		username, password, ok := r.BasicAuth()
		if ok {
            // Calculate SHA-256 hashes for the provided and expected
            // usernames and passwords.
			usernameHash := sha256.Sum256([]byte(username))
			passwordHash := sha256.Sum256([]byte(password))
			expectedUsernameHash := sha256.Sum256([]byte("your expected username"))
			expectedPasswordHash := sha256.Sum256([]byte("your expected password"))

            // Use the subtle.ConstantTimeCompare() function to check if 
            // the provided username and password hashes equal the  
            // expected username and password hashes. ConstantTimeCompare
            // will return 1 if the values are equal, or 0 otherwise. 
            // Importantly, we should to do the work to evaluate both the 
            // username and password before checking the return values to 
            // avoid leaking information.
			usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
			passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)

            // If the username and password are correct, then call
            // the next handler in the chain. Make sure to return 
            // afterwards, so that none of the code below is run.
			if usernameMatch && passwordMatch {
				next.ServeHTTP(w, r)
				return
			}
		}

        // If the Authentication header is not present, is invalid, or the
        // username or password is wrong, then set a WWW-Authenticate 
        // header to inform the client that we expect them to use basic
        // authentication and send a 401 Unauthorized response.
		w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
	})
}

You might also be wondering here what the realm value is and why we are setting it to "restricted" in the WWW-Authenticate response header.

Basically, the realm value is a string which allows you to create partitions of protected space in your application. So, for example, an application could have a "documents" realm and an "admin area" realm, which require different credentials. A web browser (or other type of client) can cache and automatically reuse the same username and password for any requests within the same realm, so that the prompt doesn't need to be shown for every single request.

If you don't require multiple partitions for your application, you can set the realm to a single hardcoded value like "restricted", like we have in the code above.

For the sake of security and/or flexibility, you may also prefer to store the expected username and password values in environment variables or pass them as command-line flag values when starting the application, rather than hard-coding them into your application.

A working example

Let's take a quick look at this in the context of a small — but fully functioning — web application.

If you'd like to follow along, create a new basic-auth-example directory on your computer, add a main.go file, initialize a module, and create a pair of locally-trusted TLS certificates using the mkcert tool. Like so:

$ mkdir basic-auth-example
$ cd basic-auth-example
$ touch main.go
$ go mod init example.com/basic-auth-example
go: creating new go.mod: module example.com/basic-auth-example
$ mkcert localhost
Created a new certificate valid for the following names 📜
     - "localhost"
    
    The certificate is at "./localhost.pem" and the key at "./localhost-key.pem" ✅
    
    It will expire on 21 September 2023 🗓
$ ls
go.mod  localhost-key.pem  localhost.pem  main.go

Then add the following code to the main.go file, so that the application reads the expected username and password from environment variables and uses the middleware pattern that we described above.

package main

import (
    "crypto/sha256"
    "crypto/subtle"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"
)

type application struct {
    auth struct {
        username string
        password string
    }
}

func main() {
    app := new(application)

    app.auth.username = os.Getenv("AUTH_USERNAME")
    app.auth.password = os.Getenv("AUTH_PASSWORD")

    if app.auth.username == "" {
        log.Fatal("basic auth username must be provided")
    }

    if app.auth.password == "" {
        log.Fatal("basic auth password must be provided")
    }

    mux := http.NewServeMux()
    mux.HandleFunc("/unprotected", app.unprotectedHandler)
    mux.HandleFunc("/protected", app.basicAuth(app.protectedHandler))

    srv := &http.Server{
        Addr:         ":4000",
        Handler:      mux,
        IdleTimeout:  time.Minute,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
    }

    log.Printf("starting server on %s", srv.Addr)
    err := srv.ListenAndServeTLS("./localhost.pem", "./localhost-key.pem")
    log.Fatal(err)
}

func (app *application) protectedHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "This is the protected handler")
}

func (app *application) unprotectedHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "This is the unprotected handler")
}

func (app *application) basicAuth(next http.HandlerFunc) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		username, password, ok := r.BasicAuth()
		if ok {
			usernameHash := sha256.Sum256([]byte(username))
			passwordHash := sha256.Sum256([]byte(password))
			expectedUsernameHash := sha256.Sum256([]byte(app.auth.username))
			expectedPasswordHash := sha256.Sum256([]byte(app.auth.password))

			usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
			passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)

			if usernameMatch && passwordMatch {
				next.ServeHTTP(w, r)
				return
			}
		}

		w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
	})
}   

You should then be able to start the application, using a pair of temporary AUTH_USERNAME and AUTH_PASSWORD environment variables. Like so:

$ AUTH_USERNAME=alice AUTH_PASSWORD=p8fnxeqj5a7zbrqp go run .
2021/06/20 16:09:21 starting server on :4000

At this point, if you open your web browser and visit https://localhost:4000/protected you should be greeted by the basic authentication prompt.

Alternatively, you can make some requests using curl to verify that the authentication checks are working correctly.

$ curl -i https://localhost:4000/unprotected
HTTP/2 200 
content-type: text/plain; charset=utf-8
content-length: 32
date: Sun, 20 Jun 2021 14:09:56 GMT

This is the unprotected handler

$ curl -i https://localhost:4000/protected
HTTP/2 401 
content-type: text/plain; charset=utf-8
www-authenticate: Basic realm="restricted", charset="UTF-8"
x-content-type-options: nosniff
content-length: 13
date: Sun, 20 Jun 2021 14:09:59 GMT

Unauthorized

$ curl -i -u alice:p8fnxeqj5a7zbrqp https://localhost:4000/protected
HTTP/2 200 
content-type: text/plain; charset=utf-8
content-length: 30
date: Sun, 20 Jun 2021 14:10:14 GMT

This is the protected handler

$ curl -i -u alice:wrongPa55word https://localhost:4000/protected
HTTP/2 401 
content-type: text/plain; charset=utf-8
www-authenticate: Basic realm="restricted", charset="UTF-8"
x-content-type-options: nosniff
content-length: 13
date: Sun, 20 Jun 2021 14:15:30 GMT

Unauthorized

Making a request to a protected resource

Finally, when you need to access a protected resource, Go makes it very straightforward. All you need to do is call the r.SetBasicAuth() method on your request before executing it. Like so:

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

func main() {
    client := http.Client{Timeout: 5 * time.Second}

    req, err := http.NewRequest(http.MethodGet, "https://localhost:4000/protected", http.NoBody)
    if err != nil {
        log.Fatal(err)
    }

    req.SetBasicAuth("alice", "p8fnxeqj5a7zbrqp")

    res, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }

    defer res.Body.Close()

    resBody, err := io.ReadAll(res.Body)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Status: %d\n", res.StatusCode)
    fmt.Printf("Body: %s\n", string(resBody))
}