Session

A session is a mechanism for storing user-specific information temporarily. It can be implemented in several ways.

For server-side sessions, when a user visits a website, the server generates a unique session ID and sends it to the client’s browser via a cookie. This session ID serves as a reference to the user’s data stored on the server.

Client-side sessions are easier to scale since they don’t require a database. One method is using signed cookies, where a signature is added to the cookie to prevent client-side tampering. However, since signed cookies are not encrypted, they should not contain sensitive information.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"net/http"
	"os"
	"time"
)

func signCookie(key string, name string, value string, expires time.Time) string {
	hash := hmac.New(sha256.New, []byte(key))
	hash.Write([]byte(name))
	hash.Write([]byte(string(expires.Unix())))
	hash.Write([]byte(value))
	return base64.URLEncoding.EncodeToString(append([]byte(value), hash.Sum(nil)...))
}

func setSignedCookie(w http.ResponseWriter, key string, name string, value string, path string, expires time.Time) {
	cookie := http.Cookie{
		Name:     name,
		Value:    signCookie(key, name, value, expires),
		Path:     path,
		HttpOnly: true,
		Secure:   true,
		Expires:  expires,
	}
	http.SetCookie(w, &cookie)
}

func getSingedCookie(r *http.Request, key string, name string) (string, error) {
	cookie, err := r.Cookie(name)
	if err != nil {
		return "", err
	}
	value, err := base64.URLEncoding.DecodeString(cookie.Value)
	if err != nil {
		return "", err
	}
	if cookie.Value == signCookie(key, cookie.Name, string(value[:len(value)-sha256.Size]), cookie.Expires) {
		return string(value[:len(value)-sha256.Size]), nil
	}
	return "", nil
}

func main() {

	key := os.Getenv("COOKIE_KEY")

	http.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query()
		setSignedCookie(w, key, "user", query.Get("user"), "/", time.Now().Add(time.Hour*1))
		fmt.Fprintln(w, "OK")
	})

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		user, err := getSingedCookie(r, key, "user")
		if err != nil {
			http.Error(w, "Forbidden", http.StatusForbidden)
			return
		}
		fmt.Fprintf(w, "hello, %s!", user)
	})

	if err := http.ListenAndServe(":3000", nil); err != nil {
		panic(err)
	}
}
curl -X POST 'localhost:3000/login?user=alice' -c cookies.txt                                                                                                                                                                                                                                                                                  <<<
OK
$ cat cookies.txt                                                                                                                                                                                                                                                                                                                                <<<
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost     FALSE   /       TRUE    1730000133      user    YWxpY2XyxknUWqVfmzlxWLYG-vFKePWo6wvg4nyzRLe-iLxPRQ==
$ curl localhost:3000 -b cookies.txt                                                                                                                                                                                                                                                                                                             <<<
hello, alice!%
$ curl -i localhost:3000
HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sun, 27 Oct 2024 02:40:40 GMT
Content-Length: 10

Forbidden