Think In Geek

In geek we trust

Setting and deleting cookies in Go

In this post we’ll learn how to set and delete cookies as part of your Go HTTP Handlers. We’ll also learn one way to test our handlers using HTTP recorders.

The Project

We will simulate a very basic web application where users can log in and log out. The login mechanism will be by setting a cookie to a particular value. It’s important to note than while using a cookie to store a user’s session is a common pattern, in this particular case we will be doing it in a very unsafe way: the cookie will not be encrypted or secured in any way other than having the httpOnly flag on and therefore it could be quite easy for an attacker to pretend to be logged in on the site by tampering it.

Our simple requirements will be as follows:

  • When a users goes to the /login page of our website, a cookie should be set with the name session, a value of logged in, and an expiry time of an hour
  • When a users goes to the /logout page of our website, the above cookie should be deleted

The code

As it’s usual for Go web servers, we will have 2 handlers: one for the login route and one for the logout route. Our project is going to look like this:

  • In the root of the project we’ll have our main.go file that will serve as the main entry point
  • In a folder called handlers we will have the 2 handlers we need, and the tests for them: login.go, login_test.go, logout.go and logout_test.go

From now on we’ll assume the code is going to be hosted at github, under this path: https://github.com/brafales/go-session. You’ll see the usual go package dependencies referencing this path all over the examples.

In Go, http.Handler is an interface type that responds to an HTTP request. In order for a type to comply with this interface, there’s only one method that needs implementing:

httpHandler.go
type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

The ResponseWriter is the object we can use in our handler to actually return a response to the request. This is the object we will typically interact to return a body, a status code, headers or cookies. On the other hand, the Request object will hold all the information we need from the incoming request, in case we need some information from it that will have an effect on how we respond to it.

Let’s go ahead and create our login handler skeleton:

handlers/login.go
1
2
3
4
5
6
7
8
9
10
package handlers
 
import (
	"net/http"
)
 
// Login is a handler that sets a logged in cookie
type Login struct
 
func (l Login) ServeHTTP(rw http.ResponseWriter, r *http.Request) {}

First we create our new type Login as a struct, and then give that type a method so it can implement the http.Handler interface. The method will do nothing for the time being. We create the type inside the handlers package, and we import the net/http package so we can reference types like http.ResponseWriter and http.Request.

Now that we have our handler ready to use, let’s have a look at our entry point to see how can we tie it to the main program. There are many ways to serve a certain route on an HTTP server by a handler. For this exercise we will just use Go’s basic building blocks from the net/http package: the http.Handle and http.ListenAndServe functions.

The http.Handle function has the following signature: func Handle(pattern string, handler Handler). Quoting from the official documentation,

Handle registers the handler for the given pattern in the DefaultServeMux. The documentation for ServeMux explains how patterns are matched.

In its most simple form, we can use it like this: http.Handle("/login", handlers.Login{}). This will send any requests coming to the /login path to our newly created handler.

Then we have http.ListenAndServe: func ListenAndServe(addr string, handler Handler) error. Quoting from the official documentation,

ListenAndServe listens on the TCP network address addr and then calls Serve with handler to handle requests on incoming connections. Accepted connections are configured to enable TCP keep-alives. Handler is typically nil, in which case the DefaultServeMux is used.

Again, in its most simple form, we can use it like this:

if err := http.ListenAndServe(":8080", nil); err != nil {
		panic(err)
}

The lines above will start an HTTP server listening on the port 8080 on our default network interface. We pass nil as the second parameter because we are not interested in a default handler and we will be registering our own.

At this stage, we could have a very small program that could look like this:

main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
 
import (
	"net/http"
 
	"github.com/brafales/go-session/handlers"
)
 
func main() {
	loginHandler := handlers.Login{}
	http.Handle("/login", loginHandler)
	if err := http.ListenAndServe(":8080", nil); err != nil {
		panic(err)
	}
}

If we compile and run this program, and point our browser to http://localhost:8080/login we should get a response with a status 200 and no body. Now that we have our skeleton ready, let’s go back to our handler and see how to set the actual cookie.

Setting a cookie

Let’s have a look at how we can set a cookie in our handler. There’s a handy method in the net/http package that will allow us do to exactly that: SetCookie. Its signature is func SetCookie(w ResponseWriter, cookie *Cookie). It takes an http.ResponseWriter and an http.Cookie pointer as parameters. As you may have guessed, we can use the handler’s ResponseWriter parameter, and we need to build our own cookie. The http.Cookie type is quite straightforward:

http.Cookie.go
type Cookie struct {
        Name  string
        Value string
 
        Path       string    // optional
        Domain     string    // optional
        Expires    time.Time // optional
        RawExpires string    // for reading cookies only
 
        // MaxAge=0 means no 'Max-Age' attribute specified.
        // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
        // MaxAge>0 means Max-Age attribute present and given in seconds
        MaxAge   int
        Secure   bool
        HttpOnly bool
        Raw      string
        Unparsed []string // Raw text of unparsed attribute-value pairs
}

So for our purposes, our handler could look like this:

handlers/login.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package handlers
 
import (
	"net/http"
)
 
// Login is a handler that sets a logged in cookie
type Login struct
 
func (l Login) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	cookie := http.Cookie{
		Name:     "session",
		Value:    "logged in",
		Domain:   "test.com",
		Path:     "/",
		MaxAge:   60 * 60,
		HttpOnly: true,
	}
	http.SetCookie(rw, &cookie)
}

Since we want our handlers to be a bit more flexible, we can make all of the cookie attributes part of the handler object. This will help us testing, and will let us eventually make changes in there quicker in the future. It’s also good practice to allow our handlers to be chained (so we can add a middleware type of functionality in our application like logging or monitoring). Let’s see what a complete login handler would look like with all these new features:

handlers/login.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package handlers
 
import (
	"net/http"
)
 
// Login is a handler that sets a logged in cookie
type Login struct {
	Name   string
	Value  string
	Path   string
	Domain string
	MaxAge int
	Next   http.Handler
}
 
func (l Login) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	cookie := http.Cookie{
		Name:     l.Name,
		Value:    l.Value,
		Domain:   l.Domain,
		Path:     l.Path,
		MaxAge:   l.MaxAge,
		HttpOnly: true,
	}
	http.SetCookie(rw, &cookie)
	l.Next.ServeHTTP(rw, r)
}

With this new version of the code we can configure our handler as we please.

The logout handler will now be straightforward. Turns out the way to delete a cookie is essentially the same as setting it. The only difference is the Max-Age attribute is set to either 0 or a negative integer. With this in mind, we can write the handler as follows:

handlers/logout.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package handlers
 
import (
	"net/http"
)
 
// Logout is a handler that clears a logged in cookie
type Logout struct {
	Name   string
	Path   string
	Domain string
	Next   http.Handler
}
 
func (l Logout) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	cookie := http.Cookie{
		Name:     l.Name,
		Value:    "",
		Domain:   l.Domain,
		Path:     l.Path,
		MaxAge:   0,
		HttpOnly: true,
	}
	http.SetCookie(rw, &cookie)
	l.Next.ServeHTTP(rw, r)
}

The only difference between the two handlers are two hard-coded values in the logout one: the Max-Age set to 0, and the Value set to an empty string.

Putting it all together

Let’s get back to our main program now that we’ve got our handlers written an have a look at how can we tie it all together into a fully functional program. There are many ways to configure a Go application, in this exercise we’ll be keeping it simple and assume we can pass in configuration options to the application via environment variables. The os pakage from the standard library provides a function called GetEnv which allows us to read an variable from the environment: func Getenv(key string) string. What we need to do now is:

  1. Get the configuration options from the environment
  2. Create a login and a logout handler with the configuration options
  3. Ensure requests to /login and /logout are served by the login and logout handlers respectively
  4. Start a new HTTP server that can start listening for requests

Here’s what a complete example looks like:

main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main
 
import (
	"fmt"
	"net/http"
	"os"
	"strconv"
 
	"github.com/brafales/go-session/handlers"
)
 
func main() {
	cookieName := os.Getenv("COOKIE_NAME")
	cookieValue := os.Getenv("COOKIE_VALUE")
	cookieDomain := os.Getenv("COOKIE_DOMAIN")
	cookiePath := os.Getenv("COOKIE_PATH")
	cookieDuration := os.Getenv("COOKIE_DURATION")
 
	cookieDurationInteger, err := strconv.Atoi(cookieDuration)
 
	if err != nil {
		panic(err)
	}
 
	handler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		rw.Write([]byte("OK"))
	})
 
	loginHandler := handlers.Login{Name: cookieName,
		Domain: cookieDomain,
		Path:   cookiePath,
		Value:  cookieValue,
		MaxAge: cookieDurationInteger,
		Next:   handler,
	}
 
	logoutHandler := handlers.Logout{
		Name:   cookieName,
		Domain: cookieDomain,
		Path:   cookiePath,
		Next:   handler,
	}
 
	http.Handle("/login", loginHandler)
	http.Handle("/logout", logoutHandler)
 
	address := fmt.Sprintf(":%s", os.Getenv("PORT"))
 
	if err := http.ListenAndServe(address, nil); err != nil {
		panic(err)
	}
}

We get all the configuration options in lines 13 to 23. It’s worth mentioning that strconv.Atoi can error, and we will panic if we don’t get a proper integer supplied in the COOKIE_DURATION environment variable. On line 25 we create a generic handler that will write the string "OK" into an HTTP response. This is the handler that we will chain after our handlers, so requests to /login and /logout will both have "OK" in their response body. To create this handler, we’ve used the http.HandlerFunc shortcut. This is an interesting and very idiomatic pattern in Go. http.HandlerFunc will take a function as a parameter. The function needs to have this signature: func(ResponseWriter, *Request). This signature happens to be the same as the ServeHTTP one from the http.Handler interface. When calling http.Handlerfunc with a given function, you’ll get back an instance of a type that implements the http.Handler interface, so it’s ready for you to use without having to go through the hassle of creating a new type. Think about it as a way to create anonymous http.Handler objects.

In lines 29 to 42 we create the login and logout handlers. Then we proceed to map the /login and /logout paths to the handlers in lines 44 and 45, and finally we start a new HTTP server listening to a configured port in lines 47 to 51.

You can now compile and run the program like this:

$>COOKIE_NAME=session COOKIE_VALUE="logged in" COOKIE_DOMAIN="test.com" COOKIE_PATH="/" COOKIE_DURATION=60 PORT=8080 ./go-session

Point your favourite browser to localhost:8080/login and hopefully you should see your cookie set (you may need to trick your browser into thinking the test.com domain points to your localhost via the /etc/hosts file). Equally, after visiting localhost:8080/logout, you should see your cookie disappear.

Testing our code

One more thing… we need to write tests for our handlers!

Testing http.Handler types may seem daunting at first, but the Go standard library provides us with some great tools to make it easier, so we don’t have to waste too much time creating test HTTP requests and responses. One of these tools is the ResponseRecorder type from the net/http/httptest package. The ReponseRecorder type implements the http.ResponseWriter interface, so we can pass them as a parameter to ServeHTTP. Once we’ve called ServeHTTP against a ResponseRecorder, we can ask the recorder useful questions like what the response code was, what headers it sent back and, what matters to us, what cookies it set, if any.

This is what a possible test file for our login handler could look like:

handlers/login_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package handlers_test
 
import (
	"bytes"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"testing"
 
	"github.com/brafales/go-session/handlers"
)
 
func TestLogin(t *testing.T) {
	expBody := []byte("test!")
 
	loginHandler := handlers.Login{
		Name:   "session",
		Value:  "logged in",
		Path:   "/",
		Domain: "test.com",
		MaxAge: 60,
		Next: http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			rw.Write(expBody)
		}),
	}
 
	testReq, err := http.NewRequest("GET", "/", nil)
	if err != nil {
		t.Errorf("Failed to create test request: %v", err)
	}
 
	recorder := httptest.NewRecorder()
	loginHandler.ServeHTTP(recorder, testReq)
	response := recorder.Result()
 
	bodyBytes, err := ioutil.ReadAll(response.Body)
	if err != nil {
		t.Errorf("Failed to read the response: %v", err)
	}
 
	if !bytes.Equal(bodyBytes, expBody) {
		t.Errorf("Unexpected response: %v", err)
	}
 
	cookies := response.Cookies()
 
	if len(cookies) != 1 {
		t.Error("Response returned more than one cookie")
	}
 
	cookie := cookies[0]
 
	if cookie.Value != "logged in" {
		t.Errorf("Cookie has the wrong value. Expected %v, got %v", "logged in", cookie.Value)
	}
 
	if cookie.Domain != "test.com" {
		t.Errorf("Cookie has the wrong domain. Expected %v, got %v", "test.com", cookie.Domain)
	}
 
	if cookie.Name != "session" {
		t.Errorf("Cookie has the wrong name. Expected %v, got %v", "session", cookie.Name)
	}
 
	if cookie.Path != "/" {
		t.Errorf("Cookie has the wrong domain. Expected %v, got %v", "/", cookie.Path)
	}
 
	if cookie.MaxAge != 60 {
		t.Errorf("Cookie has the wrong max age. Expected %v, got %v", 60, cookie.MaxAge)
	}
}

In the test we create an instance of our login handler in line 16. We use hard-coded values for our cookie attributes, and for our chained handler we again create an anonymous one that will write "test!" to the response. On line 27 we create a simple http.Request object, and then in lines 32 to 34 we create a new ResponseRecorder and pass it as a parameter to our login handler ServeHTTP method alongside our HTTP request, and store the result of the recorder.

After this, all that is left is to get the cookies from the response, assert that only one of them exists, and then proceed to check all the attributes we’re interested in using the standard Go test assertion mechanisms.

And that’s it!

If you are interested in a complete example of this code, deployable to Heroku, you can find it in this Github repository: https://github.com/brafales/go-session.

PS: you will have noticed that the login and logout handlers look almost exactly the same. It may be a good exercise for the reader to refactor them into a single handler that we can configure to behave as login or logout. Good luck!

,

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.