Documentation
¶
Overview ¶
Package session provides secure, signed cookie sessions for browser apps, standard library only.
The MVP is cookie-only: the whole session lives in an HMAC-SHA256 signed cookie, so there is no server-side store to run. It is thin and cryptographically conservative, and is a companion to token-based auth, not a replacement: session owns cookie and browser state, while an auth package owns subjects, passwords and tokens.
Manager ¶
m := session.New(secret,
session.WithName("sid"),
session.WithSecure(true), // HTTPS only in production
session.WithSameSite(http.SameSiteLaxMode),
session.WithTTL(24*time.Hour),
)
Reading and writing ¶
s := m.LoadOrNew(r) // existing session, or a fresh one
s.Set("theme", "dark")
err := m.Save(w, s) // sign and set the cookie
m.Destroy(w) // clear the cookie on logout
Load returns ErrNoSession when there is no cookie; LoadOrNew handles that for the common "start a session in this handler" case. With the middleware, handlers read the session from the context and persist changes explicitly:
http.Handle("/", m.Middleware(handler))
// inside handler:
s, _ := session.From(r.Context())
s.Set("seen", "1")
_ = m.Save(w, s)
Security ¶
The cookie is HttpOnly, SameSite=Lax and signed with HMAC-SHA256. Enable Secure in production. The payload is versioned so the format can evolve without breaking old cookies. Key rotation is built in: the primary key signs and any configured key (WithKey) verifies, so you can roll keys without invalidating live sessions.
The payload is SIGNED, NOT ENCRYPTED. Subject and every value are stored as base64(JSON): the client cannot forge them, but can read them. Do not put secrets in a session. Encrypted payloads are planned (WithEncryption).
Logout is stateless: Destroy clears the cookie in this response, but a copy the client already captured stays valid until its ExpiresAt, because there is no server-side store to revoke it. Keep the TTL short, consider WithAbsoluteTTL for a hard lifetime cap, and for immediate revocation pair this with a server-side denylist of session IDs.
A loaded session is not proof of authentication. Middleware and LoadOrNew return a fresh empty session when the cookie is missing or invalid, so a successful From does not mean the request is authenticated: check Subject (or your own flag) before granting access, or a protected route can fail open.
Scope ¶
The MVP does not include a server-side store, encrypted payloads or CSRF; those are planned. It does not do user management, password hashing or OAuth.
See DOC.md (English) and DOC.UK.md (Ukrainian) for the full reference.
Index ¶
- Variables
- type Manager
- type Option
- func WithAbsoluteTTL(d time.Duration) Option
- func WithClock(now func() time.Time) Option
- func WithDomain(domain string) Option
- func WithKey(key []byte) Option
- func WithName(name string) Option
- func WithPath(path string) Option
- func WithSameSite(mode http.SameSite) Option
- func WithSecure(secure bool) Option
- func WithTTL(d time.Duration) Option
- type Session
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoSession is returned when a request carries no session cookie. ErrNoSession = errors.New("session: no session cookie") // ErrInvalid is returned when a session cookie is malformed or fails its // signature check (tampering). ErrInvalid = errors.New("session: invalid session cookie") // ErrExpired is returned when a session is past its expiry. ErrExpired = errors.New("session: session expired") // ErrTooLarge is returned when an encoded session exceeds the cookie size // limit (about 4 KB). ErrTooLarge = errors.New("session: encoded session too large for a cookie") // ErrNilSession is returned by Save when given a nil session. ErrNilSession = errors.New("session: nil session") )
Functions ¶
This section is empty.
Types ¶
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager signs, reads and writes session cookies. The MVP is cookie-only: the whole session lives in a signed cookie. It is safe for concurrent use.
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/goloop/session"
)
func main() {
m := session.New([]byte("0123456789abcdef0123456789abcdef"),
session.WithName("sid"),
session.WithSecure(true),
session.WithTTL(24*time.Hour),
)
// Save a session.
rec := httptest.NewRecorder()
s := &session.Session{Subject: "user-1"}
s.Set("theme", "dark")
_ = m.Save(rec, s)
// Load it back on the next request.
req := httptest.NewRequest(http.MethodGet, "/", nil)
for _, c := range rec.Result().Cookies() {
req.AddCookie(c)
}
loaded, _ := m.Load(req)
fmt.Println(loaded.Subject, loaded.Get("theme"))
}
Output: user-1 dark
func New ¶
New creates a Manager with the given signing secret and options. The Secure cookie attribute defaults to true; disable it with WithSecure(false) only for local HTTP development. New panics if any signing key is shorter than 32 bytes: a weak key is a configuration error that must fail at startup.
func (*Manager) Destroy ¶
func (m *Manager) Destroy(w http.ResponseWriter)
Destroy clears the session cookie in the response. Because the session is a stateless signed cookie, Destroy cannot invalidate a copy the client already holds: a captured cookie stays valid until its ExpiresAt. Keep the TTL short, and for hard revocation pair this with a server-side denylist of session IDs.
func (*Manager) LoadOrNew ¶
LoadOrNew reads the session cookie, or returns a fresh empty session when there is none or it is invalid. It is the discoverable way to start a session in a handler that does not use Middleware - for example a login endpoint that sets the cookie:
s := m.LoadOrNew(r) s.Subject = userID m.Save(w, s)
func (*Manager) Middleware ¶
Middleware loads the session into the request context, creating a fresh empty session when there is none or it is invalid. Handlers read it with From and persist changes with Save.
type Option ¶
type Option func(*Manager)
Option configures a Manager.
func WithAbsoluteTTL ¶ added in v0.2.0
WithAbsoluteTTL caps the total lifetime of a session from its creation, independent of activity. The default TTL slides forward on every Save, so an active session can live indefinitely; with an absolute TTL a session older than CreatedAt+d is rejected on Load even if it was refreshed recently. A non-positive duration disables the cap (the default).
func WithDomain ¶
WithDomain sets the cookie Domain attribute.
func WithKey ¶
WithKey adds another key for verification, enabling key rotation: the primary key (passed to New) signs, any configured key verifies.
func WithName ¶
WithName sets the cookie name (default "session"). It panics on a name that is not a valid cookie token (RFC 6265): an invalid name makes net/http emit an empty Set-Cookie header, silently dropping the session, so it must fail at startup like a weak key.
func WithSameSite ¶
WithSameSite sets the SameSite attribute (default Lax).
func WithSecure ¶
WithSecure sets the Secure attribute (send only over HTTPS). It defaults to true; pass false only for local HTTP development.
type Session ¶
type Session struct {
ID string `json:"id"`
Subject string `json:"sub,omitempty"`
Values map[string]string `json:"val,omitempty"`
CreatedAt time.Time `json:"iat"`
ExpiresAt time.Time `json:"exp"`
}
Session is the state carried in a signed cookie. Values holds arbitrary string key/value pairs; Subject is a convenience field for the authenticated principal (it may be empty for anonymous sessions).
func (*Session) RegenerateID ¶
RegenerateID assigns a fresh session ID. Call it right after a privilege change (for example a successful login), then Save, to avoid session fixation.