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 namesession
, a value oflogged 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
andlogout_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:
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:
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:
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:
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:
So for our purposes, our handler could look like this:
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:
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:
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:
- Get the configuration options from the environment
- Create a login and a logout handler with the configuration options
- Ensure requests to
/login
and/logout
are served by the login and logout handlers respectively - Start a new HTTP server that can start listening for requests
Here’s what a complete example looks like:
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:
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:
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!