Secure cookie doesn't work with cookiejar on localhost

Avatar of the author Willem Schots
16 Apr, 2024
~2 min.
RSS

Yesterday, I was working on the end-to-end tests for my example web application, (specifically this PR). But I ran into an issue.

  • In the web browser I was able to successfully login and access a protected page.
  • Using Go code and a http.Client I got a successful response but the session cookie was not set.

After a successful login, my app sets a session cookie. This cookie has two important attributes:

  • HttpOnly: Meaning the cookie is inaccessible via Javascript.
  • Secure: Meaning the cookie should only be sent over an encrypted request via HTTPS.

You can find more information about these attributes on MDN.

Dealing with localhost

Most (all?) webbrowsers ignore the HTTPS requirement when the cookie is sent to a localhost hostname. This is to make local development easier I assume.

However, the Go net/http/cookiejar package doesn’t make this exception, it requires HTTPS even for localhost hostnames. Secure cookies are (silently) not included in such requests.

There is an open proposal to change this behavior,

Demo

main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/cookiejar"
	"time"

	"golang.org/x/net/publicsuffix"
)

func main() {
	s := &http.Server{
		Addr: ":9999",
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			fmt.Printf("cookies in request: %v\n", r.Cookies())

			// return a secure cookie
			http.SetCookie(w, &http.Cookie{
				Name:   "secureCookie",
				Value:  "hello world!",
				Secure: true,
			})
		}),
	}

	go func() {
		err := s.ListenAndServe()
		if err != nil {
			log.Fatalf("failed to listen and serve: %v", err)
		}
	}()

	// Create a HTTP client with a cookiejar.
	jar, err := cookiejar.New(&cookiejar.Options{
		PublicSuffixList: publicsuffix.List,
	})
	if err != nil {
		log.Fatalf("failed to create cookiejar: %v", err)
	}
	client := &http.Client{
		Timeout: time.Second,
		Jar:     jar,
	}

	// Do two requests.
	for i := 0; i < 2; i++ {
		res, err := client.Get("http://localhost:9999")
		if err != nil {
			log.Fatalf("failed to do request: %v", err)
		}

		defer res.Body.Close()
		fmt.Printf("cookies in response: %v\n", res.Cookies())
	}

}

If you run the example you will see that the cookies are always returned in the response, but never included in the requests.

If you change the Secure attribute to false (or remove it), you will see that the cookies are included in the second request.

My solution

In my app I added an option to disable Secure cookies for my end-to-end tests.

🎓

Subscribe to my Newsletter and Keep Learning.

Gain access to more content and get notified of the latest articles:

I send emails every 1-2 weeks and will keep your data safe. You can unsubscribe at any time.

Hello! I'm the Willem behind willem.dev

I created this website to help new Go developers, I hope it brings you some value! :)

You can follow me on Twitter/X, LinkedIn or Mastodon.

Thanks for reading!