seshcookie

package module
v3.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 10, 2025 License: MIT Imports: 21 Imported by: 0

README

GoDoc Go Report Card

Stateless, encrypted, type-safe session cookies for Go's net/http stack.

What is seshcookie?

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.

When should you use it?

  • 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.

Key Features

  • 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.Handler or a middleware constructor.

Installation

go get github.com/bpowers/seshcookie/v3

Quick Start

1. Define your session schema

Create 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.proto
2. Wrap your handlers

Wrap 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)))
3. Read, mutate, and persist sessions

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
}

API Reference (mirrors go doc)

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 (never nil) if no cookie is present. It returns ErrNoSession if the context was not seeded by seshcookie.
  • SetSession[T proto.Message](ctx context.Context, session T) error marks the session as changed so the cookie is rewritten at the end of the request.
  • ClearSession[T proto.Message](ctx context.Context) error deletes 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) and NewMiddleware[T proto.Message](key string, cfg *Config) (func(http.Handler) http.Handler, error) wrap an existing http.Handler/router. They derive an AES key from key using Argon2id and store configuration in a Handler[T] that you can pass directly to http.ListenAndServe.
  • DefaultConfig exposes the defaults used when cfg is nil (cookie name session, 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.

Config reference
  • CookieName (default "session"): cookie name.
  • CookiePath (default /): path scope.
  • HTTPOnly (default true): prevents JavaScript access.
  • Secure (default true): only send over HTTPS; disable only for local development.
  • MaxAge (default 24 * time.Hour): server-side TTL based on issuance time.

Best Practices

  • 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 Secure and HTTPOnly enabled, and terminate TLS before requests hit seshcookie. Toggle Secure off only for local HTTP development.
  • Pick a MaxAge that matches your authentication policy, and rotate the key when you need to invalidate all sessions at once.
  • Call SetSession only when data actually changes; combine with domain logic (e.g., bump visit counts, persist auth claims) to avoid needless cookie churn.
  • Use ClearSession on logout/revocation flows and pair seshcookie with CSRF protection for state-changing requests.

Security Model

  1. 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.
  2. AES-GCM authenticated encryption: Cookies cannot be forged or modified without the key; each write uses a fresh nonce.
  3. HTTPOnly + Secure by default: Protects against XSS-based theft and plaintext transport.
  4. Server-side expiry: The issued-at timestamp plus MaxAge determines validity, so clients cannot prolong sessions.
  5. 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.

How It Works

  1. Key derivation: The provided secret is transformed into an AES key via Argon2id with deterministic salt.
  2. Envelope pattern: Your protobuf session is wrapped in an internal SessionEnvelope carrying the payload and issued_at metadata.
  3. Encryption: The envelope is AES-GCM encrypted and base64-encoded into the cookie.
  4. Expiry enforcement: On each request, seshcookie checks issued_at + MaxAge before exposing the session to your handler.
  5. Write minimization: Cookies are rewritten only after SetSession or ClearSession, allowing long-lived sessions with stable issuance timestamps.

Migration from v2.x

Version 3.0 updates the module path to comply with Go's semantic import versioning requirements:

Migration steps:

  1. Update your import statements from github.com/bpowers/seshcookie to github.com/bpowers/seshcookie/v3.
  2. Run go mod tidy to update your dependencies.

That's it! The API remains the same as v2.x.

Migration from v1.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:

  1. Update imports to github.com/bpowers/seshcookie/v3.
  2. Define your session data as a protobuf message.
  3. Generate Go code with protoc.
  4. Update handler creation to use the generic type parameter.
  5. Change session access to use GetSession[T], SetSession, and ClearSession.
  6. Add error handling for NewHandler and session operations.

Example

A complete authentication example is available in the example/ directory, demonstrating:

  • Login/logout flows
  • Protobuf session messages
  • Role-based access control
  • Proper error handling

Performance

  • Minimal overhead: Only re-encodes cookies when session changes.
  • No server storage: Truly stateless, scales horizontally.
  • Efficient encoding: Protobuf is compact and fast.

License

seshcookie is offered under the MIT license; see LICENSE for details.

Documentation

Overview

Package seshcookie enables you to associate session-state with HTTP requests while keeping your server stateless. Because session-state is transferred as part of the HTTP request (in a cookie), state can be seamlessly maintained between server restarts or load balancing. It's inspired by Beaker (http://pypi.python.org/pypi/Beaker), which provides a similar service for Python webapps. The cookies are authenticated and encrypted (using AES-GCM) with a key derived using Argon2id from a string provided to the NewHandler function. This makes seshcookie reliable and secure: session contents are opaque to users and not able to be manipulated or forged by third parties.

Version 3.0 - Go Module v3

Version 3.0 updates the module path to follow Go's semantic import versioning (v3). Version 2.0/3.0 introduces a new API based on Protocol Buffers and Go generics. Session data is now strongly-typed using protobuf messages, providing better type safety and schema evolution. The library uses an envelope pattern where metadata (like issue time) is stored separately from the user's session payload.

Sessions have server-side expiry enforcement based on issue time, preventing cookie manipulation to extend session lifetime.

Basic Usage

Define your session data as a protobuf message:

syntax = "proto3";
package myapp;

message UserSession {
  string username = 1;
  int64 login_time = 2;
  repeated string roles = 3;
}

Then use seshcookie with Go generics:

package main

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

	"github.com/bpowers/seshcookie/v3"
	"myapp/pb"  // your generated protobuf package
)

type VisitedHandler struct{}

func (h *VisitedHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	if req.URL.Path != "/" {
		return
	}

	// GetSession returns a valid protobuf message, never nil
	session, err := seshcookie.GetSession[*pb.UserSession](req.Context())
	if err != nil {
		http.Error(rw, "Internal error", 500)
		return
	}

	// Modify the session
	session.Username = "alice"
	session.LoginTime = time.Now().Unix()

	// Explicitly save changes
	if err := seshcookie.SetSession(req.Context(), session); err != nil {
		http.Error(rw, "Internal error", 500)
		return
	}

	rw.Header().Set("Content-Type", "text/plain")
	rw.WriteHeader(200)
	rw.Write([]byte("Welcome " + session.Username))
}

func main() {
	key := "session key, preferably a sequence of data from /dev/urandom"

	// NewHandler now requires a type parameter
	handler, err := seshcookie.NewHandler[*pb.UserSession](
		&VisitedHandler{},
		key,
		&seshcookie.Config{
			HTTPOnly: true,
			Secure: true,
			MaxAge: 24 * time.Hour,  // Server-side expiry
		})

	if err != nil {
		log.Fatalf("NewHandler: %s", err)
	}

	if err := http.ListenAndServe(":8080", handler); err != nil {
		log.Fatalf("ListenAndServe: %s", err)
	}
}

Session Management

The API provides three main functions:

  • GetSession[T](ctx) - Retrieves session from context, auto-creates if empty
  • SetSession[T](ctx, session) - Marks session as changed for writing to cookie
  • ClearSession[T](ctx) - Clears session, causing cookie deletion

Sessions are only written to cookies when SetSession is called, preventing unnecessary cookie updates and preserving the original issue timestamp.

Security Features

  • Argon2id key derivation (memory-hard, GPU-resistant)
  • AES-GCM authenticated encryption
  • Server-side session expiry based on issue time
  • HTTPOnly and Secure cookie flags
  • Automatic nonce generation for each cookie
  • Change detection to minimize cookie writes
  • Type-safe session data via protobuf

Migration from v1.x

Version 2.0 is a breaking change that replaces the map[string]interface{} session type with strongly-typed protobuf messages. The API surface has changed significantly:

v1.x:

session := seshcookie.GetSession(ctx)
session["count"] = 1

v2.x:

session, err := seshcookie.GetSession[*MyProto](ctx)
session.Count = 1
seshcookie.SetSession(ctx, session)

Index

Constants

This section is empty.

Variables

View Source
var (
	// DefaultConfig is used as the configuration if a nil config
	// is passed to NewHandler
	DefaultConfig = &Config{
		CookieName: defaultCookieName,
		CookiePath: "/",
		HTTPOnly:   true,
		Secure:     true,
		MaxAge:     24 * time.Hour,
	}

	// ErrSessionExpired is returned when a session has expired
	ErrSessionExpired = errors.New("session expired")

	// ErrNoSession is returned when no session is present in the context
	ErrNoSession = errors.New("no session in context")

	// ErrTypeMismatch is returned when the session type doesn't match expected type
	ErrTypeMismatch = errors.New("session type mismatch")
)

Functions

func ClearSession

func ClearSession[T proto.Message](ctx context.Context) error

ClearSession clears the session from the context. This will cause the cookie to be deleted on the next response.

func GetSession

func GetSession[T proto.Message](ctx context.Context) (T, error)

GetSession retrieves the session from the context. Returns ErrNoSession if no session context is present. If the session is empty (no cookie was present), returns a new zero instance. The returned session is always a valid proto.Message that can be modified.

func NewMiddleware

func NewMiddleware[T proto.Message](key string, config *Config) (func(http.Handler) http.Handler, error)

NewMiddleware returns a middleware constructor for a new seshcookie Handler with a given encryption key and configuration. The type parameter T specifies the protobuf message type to use for sessions.

key must be non-empty and is used to derive the encryption key. config can be nil, in which case DefaultConfig is used.

Example:

mw, err := seshcookie.NewHandler[*UserSession]("my-secret-key", nil)
if err != nil {
    log.Fatal(err)
}

http.Handle("/", mw(http.HandlerFunc(myHandler))

func SetSession

func SetSession[T proto.Message](ctx context.Context, session T) error

SetSession updates the session in the context. This marks the session as changed so it will be written back to the cookie.

Types

type Config

type Config struct {
	CookieName string        // name of the cookie to store our session in
	CookiePath string        // resource path the cookie is valid for
	HTTPOnly   bool          // don't allow JavaScript to access cookie
	Secure     bool          // only send session over HTTPS
	MaxAge     time.Duration // server-side session expiry duration
}

Config provides directives to a seshcookie instance on cookie attributes, like if they are accessible from JavaScript and/or only set on HTTPS connections.

type Handler

type Handler[T proto.Message] struct {
	http.Handler
	Config Config
	// contains filtered or unexported fields
}

Handler is the seshcookie HTTP handler that provides a Session object to child handlers. It uses Go generics to provide type-safe session access.

func NewHandler

func NewHandler[T proto.Message](handler http.Handler, key string, config *Config) (*Handler[T], error)

NewHandler returns a new seshcookie Handler with a given inner handler, encryption key, and configuration. The type parameter T specifies the protobuf message type to use for sessions.

key must be non-empty and is used to derive the encryption key. config can be nil, in which case DefaultConfig is used.

Example:

handler, err := seshcookie.NewHandler[*UserSession](innerHandler, "my-secret-key", nil)
if err != nil {
    log.Fatal(err)
}

http.ListenAndServe(":8080", handler)

func (*Handler[T]) ServeHTTP

func (h *Handler[T]) ServeHTTP(rw http.ResponseWriter, req *http.Request)

Directories

Path Synopsis
internal
pb

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL