lstoll/session

Go HTTP Session Library

★ 0Forks 0GoGitHub ↗Compare

README

session

Go Reference

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/session
type 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.

Testing

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.

DBSC

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.

Contributors

lstolldependabot[bot]

Issues