Typed HTTP sessions for Go. Session data normally lives in a server-side KV
store, with encrypted cookie sessions available when needed.
go get lds.li/sessiontype SessionData struct {
UserID string `json:"user_id"`
}
sessions, err := session.NewKVManager[SessionData](session.NewMemoryKV(), nil)
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) {
sess := sessions.FromContext(r.Context())
sess.Get().UserID = "123"
sess.Reset()
})
mux.HandleFunc("POST /logout", func(w http.ResponseWriter, r *http.Request) {
sessions.FromContext(r.Context()).Delete()
})
handler := sessions.Wrap(mux)Call Reset when establishing an authenticated identity so a previously issued session ID cannot be reused after login. Reset also restarts the session lifetime.
By default a session lives 24 hours from creation (MaxLifetime). It is a session cookie (Persist off), the browser will drop it when the process ends. Persist requires MaxLifetime, in this case the cookie will be stored and last between browser processes. Set IdleTimeout if you want sliding expiry on session access.
NewMemoryKV is useful for tests and single-process development. Production
deployments normally provide a shared KV implementation. The
sqlkv package provides one for
database/sql.
Sessions use JSON encoding by default. Set Codec: session.GobCodec{} to use
gob instead. All instances sharing a store must use the same codec, and changing
it invalidates existing sessions.
Cookie-backed sessions are available when self-contained storage is useful:
aead, err := session.NewRotatingAESGCM(currentKey, previousKey)
if err != nil {
log.Fatal(err)
}
cookieSessions, err := session.NewCookieManager[SessionData](aead, nil)
if err != nil {
log.Fatal(err)
}Cookie sessions are self-contained. The server can expire the browser's copy,
but it cannot revoke a copied cookie before its expiry. Load keys from a secret
store and keep the current key first. NewRotatingAESGCM and
NewHMACSHA256Authenticator both accept older keys for rotation.
Get returns borrowed request-scoped *SessionData; it must not escape the
request or be accessed concurrently. Mutation alone does not schedule a write:
call Save after changing application data. Save snapshots the current value
for writing at response commit. Metadata-only writes
such as flashes reuse the latest loaded or explicitly saved application
payload. Mutations after Save are not included unless Save is called again.
Reset rotates the identifier and snapshots the current data. Session writes
after the response has committed panic.
The manager type parameter may be any non-pointer, non-interface type. Get
itself is never nil, but a zero-valued application type can still contain nil
maps, slices, or pointers that the application must initialize normally.
The sessiontest package attaches a session directly to a request without
running storage or cookie middleware:
req, change := sessiontest.WithSession(t, req, sessions, SessionData{UserID: "123"})
sessions.Wrap(handler).ServeHTTP(recorder, req)
if !change.Saved() {
t.Fatal("handler did not update the session")
}Options are available for new sessions and initial flash messages.
KV-backed sessions can be bound to a device using Device Bound Session Credentials:
sessions, err := session.NewKVManager[SessionData](store, &session.KVManagerOpts[SessionData]{
DBSCRefreshInterval: 10 * time.Minute,
DBSCOrigin: "https://example.com",
DBSCRegistrationPath: "/dbsc/register",
DBSCRefreshPath: "/dbsc/refresh",
})The manager serves the registration and refresh endpoints and enforces proofs on protected sessions. The implementation follows the current W3C Editor's Draft and has end-to-end coverage against Chrome.