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.
Signed Cookie
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