Documentation
¶
Overview ¶
Package sessions keeps session state on the server and gives the client only an identifier.
That division is the whole point. A cookie carrying the state itself has to be signed, has to be encrypted if any of it is private, cannot be revoked before it expires, and grows with whatever anyone thought to put in it. A cookie carrying a 256-bit random identifier can be revoked by deleting one row, tells an attacker who reads it nothing, and never grows.
store, _ := sessions.NewStore(backend, sessions.WithIdleTimeout(30*time.Minute))
session, _ := store.New(ctx, &Principal{UserID: "u_123"})
// hand session.ID to the client; sessions/http puts it in a signed cookie
session, err := store.Get(ctx, id) // ErrNotFound / ErrExpired if it is over
The three layers ¶
A Store is what callers hold: identifiers in, payloads out, expiry enforced. A Backend is where the records physically live — sessions/cache over any cache.Cache, sessions/database over SQL. sessions/http binds a Store to a signed cookie and to net/http.
The split between Store and Backend is not ceremony. The parts of a session store that are easy to get subtly wrong are the same parts in every backend: what Renew preserves, when a session counts as expired, whether a request that read a session just before sign-out can write it back afterwards. Written once in Store, they cannot differ between backends; written per backend, one of them would eventually be wrong, and the wrong one would still pass its tests.
Two timeouts ¶
Idle asks how long a user may walk away and come back. Absolute asks how long a session may exist at all — which is the only bound on a cookie somebody stole, because a thief is not idle. Either may be disabled; both may not.
Session.ExpiresAt is the earlier of the two, and is what a cookie's lifetime should be derived from so that the browser and the store agree on when the session ended.
Touching, and what it costs ¶
An idle timeout means every read is also a write, which at any real request rate is a lot of writes to say the same thing. Policy.Touch is how much of the idle window must elapse before a read bothers: with a thirty-minute idle timeout and a one-minute touch interval, one write per minute per active session instead of one per request.
The precision that buys back is a session whose idle deadline may be up to one interval stale — so it expires up to one interval *early*, never late. Early is the safe direction for a security control, which is the only reason the trade is on offer. Set Touch to zero to refresh on every read.
The store decides expiry, not the backend ¶
A backend is asked to keep each record until its deadline plus a grace period, and the store refuses the record the moment the deadline passes. So the backing store's own expiry is a garbage collector rather than a security control.
That is not incidental. Left to the backend, expiry would be evaluated by a redis server's clock or a row's timestamp instead of by the clock the policy was written against and a test can move; a shortened timeout would not apply to sessions already in flight; and a record already reclaimed cannot be told apart from one that never existed, so "you idled out" and "no such session" would be the same answer. The grace period costs retained bytes for expired sessions and buys all three back. Set it to zero with WithRetentionGrace to give up the distinction and reclaim at the deadline.
Renewal is not optional ¶
Renew rotates a session's identifier and carries the payload across. Call it on every privilege change, sign-in first among them. Without it, an identifier an attacker planted in a victim's browser before sign-in is still valid after it, and the attacker is now signed in as the victim — session fixation, which is a defect in the application rather than in the cookie.
CreatedAt survives renewal, deliberately. If it did not, an application that correctly renewed on every privilege change would thereby give its sessions an unbounded life, and the absolute timeout would quietly stop meaning anything.
Renew reports either a new identifier or an error, never both. A caller that sees an error must assume the old identifier still resolves and refuse the privilege change that prompted the renewal.
Identifiers ¶
Minted by NewID from crypto/rand, 256 bits, base64url. Not identifiers.New: an xid is a timestamp, a machine identifier, a process identifier, and a counter, which is sortable by design and guessable by construction. That is a feature everywhere else in this module and a vulnerability here.
Identifiers are bearer credentials, so nothing in this package puts one on a span or in a log line. What is attached describes a session without naming it.
Choosing a backend ¶
sessions/cache runs on any cache.Cache — redis for a fleet, memory for tests. It is the default answer: sessions are short-lived, read on every request, and a lost session is a sign-in rather than a lost record.
sessions/database survives cache loss, and is the answer when a sign-out has to be enforceable or a flush must not sign everybody out at once. It also enforces one thing the cache backend can only approximate: Update is a single UPDATE that touches nothing if the row is gone, so a request that read a session immediately before it was signed out cannot write it back afterwards. The cache backend checks first and then writes, which narrows that window to two adjacent round trips rather than closing it.
What T must be ¶
Whatever the chosen backend can round-trip: a concrete struct with exported fields. The cache backend serializes through its provider's codec (CBOR by default, gob available), the database backend through an encoding.Codec.
Every record carries a Version. A record written by a different shape reads as absent rather than being decoded into the current shape, so changing T is a wave of re-logins rather than users holding somebody else's fields. Bump recordVersion when Record itself changes shape; sessions_stale_records counts what that discards.
Watching it ¶
sessions_expired by reason: absolute or idle. A shift toward
absolute usually means the idle timeout is longer
than anyone thinks.
sessions_touch_failures idle deadlines that could not be refreshed. The
reads still succeeded; the sessions will expire on
their old schedule.
sessions_backend_errors backend health. Absent sessions are not counted
here — they are not errors.
sessions_stale_records records discarded for carrying another version;
expected to spike once after a shape change.
sessions_created new sessions.
sessions_renewed identifier rotations. Should track sign-ins; if it
does not, something is not renewing.
sessions_ended explicit sign-outs.
sessions_touches idle deadline refreshes.
sessions_latency_ms by operation: new, get, save, renew, delete.
Example ¶
The ordinary shape: establish a session after authenticating, read it back on the next request, end it on sign-out.
package main
import (
"context"
stderrors "errors"
"fmt"
"time"
"github.com/primandproper/platform-go/v12/cache/memory"
"github.com/primandproper/platform-go/v12/sessions"
sessionscache "github.com/primandproper/platform-go/v12/sessions/cache"
)
// Principal is what a session carries: whatever the application needs to know
// about who is making the request.
type Principal struct {
UserID string
Admin bool
}
// newStore builds a store over an in-memory cache, which is what a test wants.
// Production points cachecfg at redis instead.
func newStore(opts ...sessions.Option) sessions.Store[Principal] {
c, err := memory.NewInMemoryCache[sessions.Record[Principal]](time.Hour)
if err != nil {
panic(err)
}
backend, err := sessionscache.NewBackend(c)
if err != nil {
panic(err)
}
store, err := sessions.NewStore(backend, opts...)
if err != nil {
panic(err)
}
return store
}
func main() {
ctx := context.Background()
store := newStore()
session, err := store.New(ctx, &Principal{UserID: "u_123"})
if err != nil {
panic(err)
}
// session.ID is what the client gets — nothing else leaves the server.
read, err := store.Get(ctx, session.ID)
if err != nil {
panic(err)
}
fmt.Println("user:", read.Data.UserID)
if err = store.Delete(ctx, session.ID); err != nil {
panic(err)
}
_, err = store.Get(ctx, session.ID)
fmt.Println("after sign-out:", stderrors.Is(err, sessions.ErrNotFound))
}
Output: user: u_123 after sign-out: true
Index ¶
- Constants
- Variables
- func NewID(ctx context.Context) (string, error)
- type Backend
- type BackendStore
- func (s *BackendStore[T]) Close() error
- func (s *BackendStore[T]) Delete(ctx context.Context, id string) error
- func (s *BackendStore[T]) Get(ctx context.Context, id string) (*Session[T], error)
- func (s *BackendStore[T]) New(ctx context.Context, data *T) (*Session[T], error)
- func (s *BackendStore[T]) Policy() Policy
- func (s *BackendStore[T]) Renew(ctx context.Context, oldID string) (string, error)
- func (s *BackendStore[T]) Save(ctx context.Context, id string, data *T) error
- type Expiry
- type Option
- func WithAbsoluteTimeout(timeout time.Duration) Option
- func WithClock(c clock.Clock) Option
- func WithIdleTimeout(timeout time.Duration) Option
- func WithLogger(logger logging.Logger) Option
- func WithMetricsProvider(metricsProvider metrics.Provider) Option
- func WithRetentionGrace(grace time.Duration) Option
- func WithTouchInterval(interval time.Duration) Option
- func WithTracerProvider(tracerProvider tracing.Provider) Option
- type Policy
- func (p Policy) Deadline(createdAt, lastSeenAt time.Time) time.Time
- func (p Policy) Expiry(createdAt, lastSeenAt, now time.Time) Expiry
- func (p Policy) RetentionTTL(createdAt, now time.Time) time.Duration
- func (p Policy) ShouldTouch(lastSeenAt, now time.Time) bool
- func (p Policy) TTL(createdAt, now time.Time) time.Duration
- func (p Policy) Validate() error
- type Record
- type Session
- type Store
Examples ¶
Constants ¶
const ( // DefaultAbsoluteTimeout is how long a session may live from the moment it // was established, regardless of activity. It is the ceiling a stolen // cookie cannot outlive, so it is the one timeout that has to be set even // on a store nobody idles out. DefaultAbsoluteTimeout = 24 * time.Hour // DefaultIdleTimeout is how long a session survives without being read. DefaultIdleTimeout = 30 * time.Minute // DefaultTouchInterval is how much of the idle window has to elapse before // a read refreshes the session's idle deadline. See Policy for why this is // not zero. DefaultTouchInterval = time.Minute // DefaultRetentionGrace is how long an expired record is kept before the // backing store may reclaim it, so that a user who comes back can be told // why they were signed out rather than merely that they were. See // Policy.Grace for why a backend's own expiry is the wrong thing to end a // session with. DefaultRetentionGrace = time.Hour // DefaultIDByteLength is how many random bytes a session identifier is // minted from — 256 bits, which is what makes the cache backend's // collision-free Create safe to assume. DefaultIDByteLength = 32 )
Variables ¶
var ( // ErrNotFound indicates no session is stored under the identifier. It is // also what a record written by another shape of this package reads as, // deliberately: a stale record is a re-login, and misreading one would hand // a user a payload decoded from bytes that meant something else. ErrNotFound = platformerrors.New("session not found") // ErrExpired indicates a session was found but is past one of its // deadlines. It wraps ErrNotFound, so a caller that does not care why the // session is unusable checks only that. ErrExpired = platformerrors.Wrap(ErrNotFound, "session expired") // ErrIdleTimeout indicates the session went unread for longer than the // idle timeout. It wraps ErrExpired. ErrIdleTimeout = platformerrors.Wrap(ErrExpired, "session idle timeout elapsed") // ErrAbsoluteTimeout indicates the session outlived its absolute timeout, // which no amount of activity extends. It wraps ErrExpired. ErrAbsoluteTimeout = platformerrors.Wrap(ErrExpired, "session absolute timeout elapsed") // ErrIDConflict indicates Create was given an identifier that already // exists. Identifiers are 256 bits of cryptographic randomness, so this // means a backend was handed an identifier it did not mint, not that two // sessions collided. ErrIDConflict = platformerrors.New("session identifier already in use") // ErrIDRequired indicates an empty identifier was supplied. It wraps // errors.ErrEmptyInputParameter, so a caller may check either. ErrIDRequired = platformerrors.Wrap(platformerrors.ErrEmptyInputParameter, "empty session identifier") // ErrNilBackend indicates NewStore was called without a backend. It wraps // errors.ErrNilInputParameter, so a caller may check either. ErrNilBackend = platformerrors.Wrap(platformerrors.ErrNilInputParameter, "nil session backend") // ErrNoTimeout indicates a Policy with neither an absolute nor an idle // timeout. Such a store never releases a session, so it is rejected at // construction rather than discovered as unbounded growth. ErrNoTimeout = platformerrors.New("session policy sets no timeout") // ErrTouchExceedsIdleTimeout indicates a touch interval at least as long as // the idle timeout, which would let a session idle out between the reads // that were supposed to keep it alive. ErrTouchExceedsIdleTimeout = platformerrors.New("session touch interval is not shorter than the idle timeout") // ErrNegativeTouchInterval indicates a negative touch interval. Zero is // meaningful — refresh on every read — so it cannot stand in for "unset". ErrNegativeTouchInterval = platformerrors.New("negative session touch interval") )
Sentinels. errors/http maps these onto status codes, so that package imports this one. That direction is load-bearing: nothing here may import errors/http or errors/grpc, or the cycle closes.
The four absence errors form a chain — ErrIdleTimeout and ErrAbsoluteTimeout wrap ErrExpired, which wraps ErrNotFound — so a caller picks the resolution it cares about. A middleware deciding whether to redirect to a login page checks ErrNotFound; a page that wants to say "you were signed out for inactivity" checks ErrIdleTimeout.
Functions ¶
func NewID ¶
NewID mints a session identifier: DefaultIDByteLength bytes from the process's secure random source, base64url-encoded so it travels in a cookie unescaped.
It deliberately does not use identifiers.New. That mints an xid, which is a timestamp, a machine identifier, a process identifier, and a counter — sortable by design and therefore guessable by construction. Everywhere else in this module that is a feature; here it would mean an attacker who holds one identifier can enumerate the ones minted around it. A session identifier is a bearer credential and has to come from crypto/rand.
The generator is package-level rather than injected. There is exactly one correct source of randomness for this value, and an option to replace it would be an option to weaken it.
Types ¶
type Backend ¶
type Backend[T any] interface { // Load reads the record stored under id, reporting ErrNotFound when // there is none. It does not evaluate expiry — the Store does, from the // record's own anchors, so both backends answer the same question the // same way. Load(ctx context.Context, id string) (*Record[T], error) // Create stores a record under an identifier that must not already // exist, reporting ErrIDConflict if it does. Create(ctx context.Context, id string, record *Record[T], ttl time.Duration) error // Update overwrites the record stored under an existing identifier, // reporting ErrNotFound when there is none. // // The existence requirement is not bookkeeping. Without it a request // that read a session just before it was signed out would write it back // afterwards, and the sign-out would not have happened. Update(ctx context.Context, id string, record *Record[T], ttl time.Duration) error // Rename moves a record from oldID to newID, reporting ErrNotFound when // oldID holds nothing. On a nil return, oldID no longer resolves. Rename(ctx context.Context, oldID, newID string, record *Record[T], ttl time.Duration) error // Delete removes the record stored under id. An identifier that was // already absent is not an error. Delete(ctx context.Context, id string) error // Close releases the backend's resources and is safe to call more than // once. Close() error }
Backend is where a Store's records physically live. sessions/cache and sessions/database implement it; a Store adds identifiers, expiry, and observability on top.
The split exists because the parts worth getting exactly right — what Renew preserves, when a session is expired, whether a touch may resurrect a signed-out session — are the parts that must not differ between backends. Written once in Store, they cannot.
Every method's ttl is how long the record should remain retrievable, and is always positive: a Store never asks a backend to store something already expired.
type BackendStore ¶
type BackendStore[T any] struct { // contains filtered or unexported fields }
BackendStore is the one Store implementation: a Policy, an identifier mint, and a Backend. It is exported, and returned by NewStore, so a caller can depend on the store it built rather than on the Store seam.
func NewStore ¶
func NewStore[T any](backend Backend[T], opts ...Option) (*BackendStore[T], error)
NewStore builds a Store over a Backend.
The Backend is required and has no default. An implicit in-memory one would work in every test and lose every session on deploy in production, which is the failure mode that looks like intermittent sign-outs for a week before anyone finds it.
func (*BackendStore[T]) Delete ¶
func (s *BackendStore[T]) Delete(ctx context.Context, id string) error
Delete ends a session.
func (*BackendStore[T]) Get ¶
func (s *BackendStore[T]) Get(ctx context.Context, id string) (*Session[T], error)
Get reads a session and refreshes its idle deadline when the touch interval has elapsed.
func (*BackendStore[T]) New ¶
func (s *BackendStore[T]) New(ctx context.Context, data *T) (*Session[T], error)
New establishes a session around data.
func (*BackendStore[T]) Policy ¶
func (s *BackendStore[T]) Policy() Policy
Policy reports the expiry rule this store enforces.
type Expiry ¶
type Expiry uint8
Expiry names which of a Policy's two deadlines a session has passed.
const ( // ExpiryNone means the session is still live. ExpiryNone Expiry = iota // ExpiryAbsolute means the session outlived its absolute timeout, measured // from when it was established. No activity extends this one. ExpiryAbsolute // ExpiryIdle means the session went unread for longer than the idle // timeout. ExpiryIdle )
type Option ¶
type Option func(*storeOptions)
Option configures a Store at construction.
It is deliberately not parameterized on the Store's T. None of these settings depend on it, and Go cannot infer a type argument from a call's result type — so an Option[T] would force every call site to spell the payload type out by hand, WithIdleTimeout[Principal](time.Hour), forever.
func WithAbsoluteTimeout ¶
WithAbsoluteTimeout bounds a session's total lifetime, measured from when it was established and unaffected by activity or by Renew.
A non-positive value disables it, which is only safe when the idle timeout is not also disabled — a store with neither never releases a session, and is rejected at construction.
func WithClock ¶
WithClock swaps the clock the store stamps and expires against, so timeout behavior is deterministic in tests.
func WithIdleTimeout ¶
WithIdleTimeout bounds how long a session may go unread. A non-positive value disables it, and also disables touching: there is then no idle deadline for a read to refresh.
func WithLogger ¶
WithLogger attaches a logger. An absent logger logs nowhere.
func WithMetricsProvider ¶
WithMetricsProvider attaches a metrics provider. An absent one records nothing.
func WithRetentionGrace ¶
WithRetentionGrace sets how long an expired record is kept before the backing store may reclaim it. See Policy.Grace for what it buys; a non-positive value lets the backend reclaim the record at the deadline, so an expired session then reads as merely absent.
func WithTouchInterval ¶
WithTouchInterval sets how much of the idle window must elapse before a read refreshes the idle deadline. Zero refreshes on every read; see Policy for what the interval buys and what it costs.
func WithTracerProvider ¶
WithTracerProvider attaches a tracer provider. An absent one traces nowhere.
type Policy ¶
type Policy struct {
// Absolute bounds a session's total lifetime from CreatedAt. Non-positive
// disables it.
Absolute time.Duration
// Idle bounds how long a session may go unread. Non-positive disables it.
Idle time.Duration
// Touch is how much of the idle window must elapse before a read refreshes
// the idle deadline.
//
// It exists because an idle timeout is otherwise a write on every read. At
// a hundred requests a second against one session that is a hundred writes
// a second to say the same thing; with a touch interval it is one write per
// interval. What it costs is precision: a session's idle deadline can be up
// to one interval stale, so a session expires up to Touch early rather than
// late. Early is the safe direction for a security control, which is why
// the trade is available at all.
//
// Zero refreshes on every read. It must be shorter than Idle, and is
// irrelevant when Idle is disabled — there is no idle deadline to refresh,
// so nothing is ever touched.
Touch time.Duration
// Grace is how long an expired record is kept before the backing store is
// allowed to reclaim it.
//
// It exists because a backend's own expiry would otherwise decide when
// sessions end, and it is the wrong thing to decide it. A record a cache
// has already dropped cannot be told apart from one that never existed, so
// a user who idled out and a client presenting a forged identifier would
// get the same answer; worse, expiry would then be evaluated by the cache
// server's clock rather than by the store's, which is neither the clock the
// policy was written against nor one a test can move.
//
// So every write asks the backend to keep the record for its deadline plus
// this, and the store refuses it the moment the deadline passes. The
// backend's expiry becomes a garbage collector rather than a security
// control. What it costs is retained bytes for expired sessions; what it
// buys is a deterministic timeout and a returning user who can be told why
// they were signed out.
//
// Non-positive lets the backend reclaim the record exactly at the deadline,
// which is the cheaper setting and the one that gives up the distinction.
Grace time.Duration
}
Policy is the expiry rule a Store enforces, and the reason both backends cannot disagree about when a session ends: they never evaluate it, the Store does, once.
Two timeouts, because they answer different questions. Idle asks how long a user may walk away and come back; Absolute asks how long a session may exist at all, which is the only bound on a cookie somebody stole. Either may be disabled by setting it non-positive, but not both — see ErrNoTimeout.
func (Policy) Deadline ¶
Deadline is the instant a session stops being usable if nothing touches it again: the earlier of the two deadlines, or the only one that is enabled.
func (Policy) Expiry ¶
Expiry reports which deadline, if either, now has passed.
Absolute is checked first so that a session past both is reported as the one nothing could have prevented. Telling a user they were signed out for inactivity when they were in fact signed out on schedule is a worse answer than the reverse.
func (Policy) RetentionTTL ¶
RetentionTTL is how long a backend should keep a record written now: its remaining life, plus the grace that lets an expired session still be diagnosed rather than merely missed. See Policy.Grace.
It is what a Store hands a Backend. TTL is the deadline the Store itself enforces, and the two are deliberately different numbers.
func (Policy) ShouldTouch ¶
ShouldTouch reports whether a read should refresh the idle deadline.
It is false whenever the idle timeout is disabled: with no idle deadline there is nothing for a touch to extend, and writing on every read to update a field nobody expires against is pure cost.
func (Policy) TTL ¶
TTL is how much longer a record written now should remain retrievable: the idle window, clipped to whatever is left of the absolute one.
It is what a Store hands a Backend, so the backing store's own expiry lands on the same instant Deadline reports. A non-positive result means the session is already over and must not be written at all — callers reach Expiry first, which is where that is decided.
type Record ¶
type Record[T any] struct { // CreatedAt is when the session was established. It survives Renew, so // rotating an identifier does not extend the absolute deadline — which // is the whole reason rotation is safe to do on every privilege change. CreatedAt time.Time // LastSeenAt is when the session was last read or written. It is the // idle deadline's anchor, and it is refreshed no more often than the // Policy's touch interval — see Policy. LastSeenAt time.Time // Data is the payload. It may be nil: a session that only needs to // exist is a legitimate session. Data *T // Version is the record shape this was written with. Version int }
Record is what a Backend holds for a session identifier. It carries the payload and the two anchors expiry is measured from, and nothing else — the identifier is the key it is stored under, and the deadlines are derived from the Policy rather than frozen into the record.
T must round-trip through whichever backend stores it. The cache backend serializes with its provider's codec (CBOR by default), the database backend with an encoding.Codec; both want a concrete struct with exported fields.
type Session ¶
type Session[T any] struct { // CreatedAt is when the session was established, unchanged by Renew. CreatedAt time.Time // LastSeenAt is the idle deadline's anchor as of this read. LastSeenAt time.Time // ExpiresAt is the earlier of the absolute and idle deadlines: the // instant this session stops being usable if nothing touches it again. // It is what a cookie's MaxAge should be derived from, so the browser // and the store agree on when the session ended. ExpiresAt time.Time // Data is the payload, as stored. Data *T // ID is the identifier this session was read under. It is the value the // cookie carries, and the only part of a session that ever leaves the // server. ID string }
Session is a live session as a Store hands it back.
It is a snapshot, not a handle: mutating it changes nothing server-side. Store.Save is how a payload is written back.
type Store ¶
type Store[T any] interface { // New establishes a session around data and returns it, identifier // included. data may be nil. New(ctx context.Context, data *T) (*Session[T], error) // Get reads a session, refreshing its idle deadline when the Policy's // touch interval has elapsed. // // A session past either deadline is reported as ErrExpired and removed; // one that was never there, or whose record was written by another // shape of this package, is reported as ErrNotFound. Get(ctx context.Context, id string) (*Session[T], error) // Save replaces a session's payload. It refreshes the idle deadline and // leaves the absolute one alone. Save(ctx context.Context, id string, data *T) error // Renew rotates a session's identifier, carrying the payload and the // original CreatedAt across, and returns the new identifier. // // Call it on every privilege change — sign-in above all. Without it, an // identifier an attacker planted before sign-in is still valid after // it, which is session fixation. Because CreatedAt survives, rotating // on every privilege change cannot be used to extend a session forever. // // The old identifier stops working the moment this returns nil. If it // returns an error, assume it still works and refuse the privilege // change. Renew(ctx context.Context, oldID string) (newID string, err error) // Delete ends a session. An identifier that was already gone is not an // error: sign-out is not the place to surface a race. Delete(ctx context.Context, id string) error // Policy reports the expiry rule this store enforces. // // It is on the interface because the cookie has to agree with it: // sessions/http derives how long a browser should keep a session // cookie from the absolute timeout rather than from a second setting // that could drift from this one. Policy() Policy // Close releases what the store holds — the backend's connection pool, // a background sweep — and is safe to call more than once. Close() error }
Store is the server-side session store: identifiers in, payloads out.
Every method takes or returns an identifier rather than a cookie. What the identifier travels in is the caller's business — sessions/http binds it to a signed cookie, which is what nearly everyone wants.
A Store enforces the expiry Policy and mints identifiers; where the records physically live is the Backend's business. Absence and expiry are reported as ErrNotFound and ErrExpired, and ErrExpired wraps ErrNotFound, so a caller that does not care about the difference checks one thing.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cache stores session records in a cache.Cache.
|
Package cache stores session records in a cache.Cache. |
|
Package sessionscfg assembles a session store, and optionally a cookie-bound manager, from environment configuration.
|
Package sessionscfg assembles a session store, and optionally a cookie-bound manager, from environment configuration. |
|
Package database stores session records in a SQL table.
|
Package database stores session records in a SQL table. |
|
migrations
Package migrations supplies the session table's DDL, rendered for a dialect and table prefix.
|
Package migrations supplies the session table's DDL, rendered for a dialect and table prefix. |
|
Package http binds a sessions.Store to a signed cookie and to net/http.
|
Package http binds a sessions.Store to a signed cookie and to net/http. |
|
Package sessionsmock provides moq-generated mock implementations of interfaces in the sessions package.
|
Package sessionsmock provides moq-generated mock implementations of interfaces in the sessions package. |