go 142 lines · 3 tabs

Stateless Session Cookies Signed and Verified With HMAC in Go

Shared by codesnips Jul 2026
3 tabs
package session

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"strings"
)

var (
	ErrMalformed = errors.New("session: malformed token")
	ErrBadSig    = errors.New("session: signature mismatch")
)

type Signer struct {
	key []byte
}

func NewSigner(key []byte) *Signer {
	return &Signer{key: key}
}

func (s *Signer) mac(encoded string) []byte {
	h := hmac.New(sha256.New, s.key)
	h.Write([]byte(encoded))
	return h.Sum(nil)
}

func (s *Signer) Sign(payload []byte) string {
	encoded := base64.RawURLEncoding.EncodeToString(payload)
	sig := base64.RawURLEncoding.EncodeToString(s.mac(encoded))
	return encoded + "." + sig
}

func (s *Signer) Verify(token string) ([]byte, error) {
	encoded, sigPart, ok := strings.Cut(token, ".")
	if !ok {
		return nil, ErrMalformed
	}
	gotSig, err := base64.RawURLEncoding.DecodeString(sigPart)
	if err != nil {
		return nil, ErrMalformed
	}
	if !hmac.Equal(gotSig, s.mac(encoded)) {
		return nil, ErrBadSig
	}
	return base64.RawURLEncoding.DecodeString(encoded)
}
3 files · go Explain with highlit

A stateless session avoids server-side storage by putting the session data directly in the cookie and protecting it with a keyed message authentication code. This snippet shows how to encode, sign, and verify such a cookie in plain Go using crypto/hmac and crypto/sha256, then wire it into an net/http middleware.

In signer.go, the Signer type wraps a secret key and produces a token of the form payload.signature. Sign base64url-encodes the raw payload, computes an HMAC-SHA256 over that encoded string, and appends the encoded MAC. Verify reverses the process but is careful about two things that trip people up. First, it recomputes the expected MAC over the encoded payload rather than the raw bytes, so the signed input is unambiguous. Second, it compares MACs with hmac.Equal, a constant-time comparison that prevents timing attacks — using == or bytes.Equal here would leak how many leading bytes matched. The mac helper centralizes MAC creation so signing and verifying can never diverge.

session.go layers a typed session on top of the raw signer. Session is a small struct with a UID, an Issued timestamp, and an Expires deadline; Encode marshals it to JSON and hands the bytes to Sign. Decode verifies the signature first, then unmarshals, then checks expiry with time.Now().After(s.Expires). The ordering matters: expiry is only trusted after the signature proves the payload was not tampered with, since the expiry lives inside the signed blob. This is the core trade-off of stateless sessions — anyone can read the contents (it is only signed, not encrypted), so no secrets belong in the payload, and revocation before expiry is impossible without extra state.

middleware.go connects this to HTTP. Load reads the session cookie, decodes it, and stashes the result in the request context so handlers can call FromContext. Issue writes a fresh cookie with HttpOnly, Secure, and SameSiteLaxMode set, which blocks JavaScript access and cross-site leakage. A rotated secret invalidates every outstanding cookie at once, which is the intended kill switch when a key is compromised.


Related snips

Share this code

Here's the card — post it anywhere.

Stateless Session Cookies Signed and Verified With HMAC in Go — share card
Link copied