Stateless, encrypted, type-safe session cookies for Go's
net/httpstack.
github.com/bpowers/seshcookie/v3. See Migration below. The API matches v2.x (generics + protobuf sessions), so only the module path changes when upgrading from v2.
seshcookie enables you to associate session-state with HTTP requests while keeping your server stateless. Session data travels with each request inside a single AES-GCM encrypted cookie, so restarts, blue/green deploys, or load-balanced replicas do not require sticky routing or a cache tier. The package is inspired by Beaker and mirrors the authoritative go doc github.com/bpowers/seshcookie/v3 description: cookies are authenticated/encrypted with a key derived via Argon2id every time NewHandler/NewMiddleware is constructed. Each request gets a strongly-typed protobuf message via context.Context; mutate it, call SetSession, and seshcookie handles encryption, authentication, expiry, and change detection for you.
- You want "sticky" session behavior for horizontally scaled/stateless Go services or serverless functions.
- Your session payload is small (fits comfortably inside a few kilobytes) and naturally modeled as a protobuf message.
- You would rather avoid provisioning Redis or another backing store just to hold session blobs.
If you need to centrally revoke sessions, store large payloads, or share state with non-HTTP clients, a server-side store may be a better fit.
- Type-Safe Sessions: Protocol Buffers + Go generics provide compile-time schemas.
- Secure by Default: Argon2id key derivation, AES-GCM encryption, Secure + HTTPOnly cookies.
- Server-Side Expiry: Sessions expire based on the issuance timestamp, not browser-controlled metadata.
- Stateless Scalability: No shared storage or sticky routing; any replica can serve any request.
- Change Detection: Cookies are only rewritten when session data actually changes via
SetSession. - Flexible Integration: Use either a pre-wrapped
http.Handleror a middleware constructor.
go get github.com/bpowers/seshcookie/v3Create a .proto file:
syntax = "proto3";
package myapp;
option go_package = "myapp/pb";
message UserSession {
string username = 1;
int32 visit_count = 2;
repeated string roles = 3;
}Generate Go code:
protoc --go_out=. --go_opt=paths=source_relative session.protoWrap your top-level handler (or router) with seshcookie. Provide a high-entropy key that is shared by every replica of your service.
key := os.Getenv("SESHCOOKIE_KEY") // base64 string holding 32 random bytes
handler, err := seshcookie.NewHandler[*pb.UserSession](
&VisitedHandler{},
key,
&seshcookie.Config{
HTTPOnly: true,
Secure: true,
MaxAge: 24 * time.Hour,
},
)
if err != nil {
log.Fatalf("NewHandler: %v", err)
}
log.Fatal(http.ListenAndServe(":8080", handler))Prefer middleware-style wiring when you already have a router (e.g., http.ServeMux, chi, gorilla/mux):
mw, err := seshcookie.NewMiddleware[*pb.UserSession](key, nil)
if err != nil {
log.Fatal(err)
}
router := http.NewServeMux()
router.HandleFunc("/", appHandler)
log.Fatal(http.ListenAndServe(":8080", mw(router)))Within any wrapped handler, call the helpers on the request context. The session is lazily created on first access and only written back when SetSession (or ClearSession) is invoked.
session, err := seshcookie.GetSession[*pb.UserSession](req.Context())
if err != nil {
http.Error(rw, "session unavailable", http.StatusInternalServerError)
return
}
session.VisitCount++
if err := seshcookie.SetSession(req.Context(), session); err != nil {
http.Error(rw, "could not save session", http.StatusInternalServerError)
return
}
if shouldLogout(req) {
_ = seshcookie.ClearSession[*pb.UserSession](req.Context()) // drops cookie at end of request
http.Redirect(rw, req, "/login", http.StatusSeeOther)
return
}go doc github.com/bpowers/seshcookie/v3 is the source of truth for exported API semantics. The key entry points are:
GetSession[T proto.Message](ctx context.Context) (T, error)retrieves the typed protobuf message from context, auto-creating a zero instance (nevernil) if no cookie is present. It returnsErrNoSessionif the context was not seeded by seshcookie.SetSession[T proto.Message](ctx context.Context, session T) errormarks the session as changed so the cookie is rewritten at the end of the request.ClearSession[T proto.Message](ctx context.Context) errordeletes the session and instructs the response writer to expire the cookie.NewHandler[T proto.Message](handler http.Handler, key string, cfg *Config) (*Handler[T], error)andNewMiddleware[T proto.Message](key string, cfg *Config) (func(http.Handler) http.Handler, error)wrap an existinghttp.Handler/router. They derive an AES key fromkeyusing Argon2id and store configuration in aHandler[T]that you can pass directly tohttp.ListenAndServe.DefaultConfigexposes the defaults used whencfgisnil(cookie namesession, path/,HTTPOnly: true,Secure: true,MaxAge: 24 * time.Hour).
Sessions live in request context until you call SetSession or ClearSession, so read-only requests avoid cookie writes and preserve the original issued_at timestamp.
CookieName(default"session"): cookie name.CookiePath(default/): path scope.HTTPOnly(defaulttrue): prevents JavaScript access.Secure(defaulttrue): only send over HTTPS; disable only for local development.MaxAge(default24 * time.Hour): server-side TTL based on issuance time.
- Generate the key from
crypto/rand(32+ bytes), store it outside source control, and keep it consistent across replicas so cookies remain decryptable everywhere. - Keep sessions compact (IDs, roles, tokens) rather than entire user profiles; browser cookies cap around 4 KB.
- Leave
SecureandHTTPOnlyenabled, and terminate TLS before requests hit seshcookie. ToggleSecureoff only for local HTTP development. - Pick a
MaxAgethat matches your authentication policy, and rotate the key when you need to invalidate all sessions at once. - Call
SetSessiononly when data actually changes; combine with domain logic (e.g., bump visit counts, persist auth claims) to avoid needless cookie churn. - Use
ClearSessionon logout/revocation flows and pair seshcookie with CSRF protection for state-changing requests.
- Argon2id-derived keys: Your secret string is stretched with Argon2id into an AES-128 key (salt deterministically derived from the secret), providing defense-in-depth even if the secret has uneven entropy.
- AES-GCM authenticated encryption: Cookies cannot be forged or modified without the key; each write uses a fresh nonce.
- HTTPOnly + Secure by default: Protects against XSS-based theft and plaintext transport.
- Server-side expiry: The issued-at timestamp plus
MaxAgedetermines validity, so clients cannot prolong sessions. - Change detection: Sessions are only re-encrypted when data changes, keeping cookies stable and reducing risk from replay of stale values.
You still need standard web security measures (TLS, CSRF tokens, input validation) around your application logic.
- Key derivation: The provided secret is transformed into an AES key via Argon2id with deterministic salt.
- Envelope pattern: Your protobuf session is wrapped in an internal
SessionEnvelopecarrying the payload andissued_atmetadata. - Encryption: The envelope is AES-GCM encrypted and base64-encoded into the cookie.
- Expiry enforcement: On each request, seshcookie checks
issued_at + MaxAgebefore exposing the session to your handler. - Write minimization: Cookies are rewritten only after
SetSessionorClearSession, allowing long-lived sessions with stable issuance timestamps.
Version 3.0 updates the module path to comply with Go's semantic import versioning requirements:
Migration steps:
- Update your import statements from
github.com/bpowers/seshcookietogithub.com/bpowers/seshcookie/v3. - Run
go mod tidyto update your dependencies.
That's it! The API remains the same as v2.x.
Version 2.0/3.0 is a breaking change from v1.x. Key differences:
| v1.x | v2.x/v3.x |
|---|---|
Session map[string]interface{} |
Strongly-typed protobuf messages |
GetSession(ctx) Session |
GetSession[T](ctx) (T, error) |
| Direct map modification | Explicit SetSession(ctx, session) |
NewHandler(h, key, cfg) *Handler |
NewHandler[T](h, key, cfg) (*Handler[T], error) |
| No expiry enforcement | Server-side expiry via MaxAge |
| GOB encoding | Protobuf encoding |
Migration steps:
- Update imports to
github.com/bpowers/seshcookie/v3. - Define your session data as a protobuf message.
- Generate Go code with
protoc. - Update handler creation to use the generic type parameter.
- Change session access to use
GetSession[T],SetSession, andClearSession. - Add error handling for
NewHandlerand session operations.
A complete authentication example is available in the example/ directory, demonstrating:
- Login/logout flows
- Protobuf session messages
- Role-based access control
- Proper error handling
- Minimal overhead: Only re-encodes cookies when session changes.
- No server storage: Truly stateless, scales horizontally.
- Efficient encoding: Protobuf is compact and fast.
seshcookie is offered under the MIT license; see LICENSE for details.