vukem.DefaultMiddleware installs the session middleware on the router stack; app.Sessions() builds the *session.Manager from SESSION_* variables.
Configuration#
| Variable | Default | Notes |
|---|---|---|
SESSION_DRIVER | cookie | cookie (data encrypted in the cookie), memory or database (the app database); Redis is installed with vukem.WithSessionStore from drivers/redis, which takes precedence |
SESSION_LIFETIME | 24h | Absolute lifetime |
SESSION_IDLE_TIMEOUT | 2h | Expires after inactivity |
SESSION_COOKIE | vukem_session | Cookie name |
SESSION_SECURE | auto | auto = secure in production or when APP_URL is https:// |
SESSION_RECORD_CLIENT | false | Keep the user agent and client network with each logged-in server-side session, for the active sessions list |
Stores#
| Store | Package | Notes |
|---|---|---|
| Cookie | session | AES-256-GCM with a key derived from APP_KEY (HKDF); tampered cookies are rejected and logged as session.invalid |
| Memory | session.NewMemoryStore() | Tests and development |
| Database | SESSION_DRIVER=database, or session.NewSQLStore(db, opts...) | sessions table in the app database (SQLite, PostgreSQL, MySQL), created at boot. Rows are keyed by SHA-256 of the session id, so the table holds no usable cookie, and carry the user id for RevokeOthers and UserSessions. Expired rows are deleted by session:prune, which the app schedules hourly; serve runs the scheduler in process (unless SCHEDULE_IN_SERVE=false, then run schedule:work or schedule:run from cron) |
| Redis | drivers/redis | vukem.WithSessionStore(rc.SessionStore("")); records the owner of each session, so revoking and listing work as on the database store |
Usage#
func (a *AuthController) Login(c *web.Ctx) error {
// …verify credentials…
s := c.Session()
s.Regenerate() // new id, keep data: prevents session fixation
s.Put("user_id", user.ID)
_ = c.Flash("success", "Welcome back!")
return c.RedirectRoute("dashboard")
}| Method | Meaning |
|---|---|
Get(key), Put(key, v), Forget(keys...), Pull(key) | Read, write, remove, read-and-remove (values must be JSON-compatible) |
Flash(key, v) | Available on the next request only |
Regenerate() | New id, same data: call on login and privilege changes |
Invalidate() | Clear everything and issue a new id: call on logout |
c.Flash(key, v) returns an error when no session middleware is installed; c.FlashValue(key) reads a flashed value.
Revoking a user's other sessions#
There are two mechanisms, and the install:auth scaffold uses both on password reset (every session) and when two-factor authentication is turned on (every session but the current one), through Accounts.RevokeOtherSessions.
Session version (every driver)#
A user type that implements auth.SessionVersioner (AuthSessionVersion() int64, a session_version column in the scaffold) has its version recorded in the session on login. SessionGuard loads the user on every request anyway, so comparing costs nothing: a session whose recorded version differs from the user's is signed out (auth.session_revoked security event). Bump the version to end every session of the user on every driver, encrypted cookies included:
_, err := db.ExecContext(ctx, "UPDATE users SET session_version = session_version + 1 WHERE id = ?", id)
// keep the current session signed in (2FA turned on), after the bump:
err = webGuard.RefreshSession(c)- A stale session is invalidated whole (data, owner and every guard's login), not just signed out of the guard.
- Declare
AuthSessionVersionon the value receiver when the guard holds values (SessionGuard[models.User]):NewSessionGuardpanics when only the pointer implements it, since the version would never be checked. - Fail closed: an unreadable recorded version counts as stale. A session logged in before the user type had a version carries none, which counts as
0: it lasts until the first bump. - A bump ends sessions at their next request; it does not delete server-side records (they expire). Remember-me tokens are bound to the version they were issued under, so a remember cookie issued before the bump no longer signs in;
RefreshSessionissues the current device a new one. API tokens are not affected (the scaffold deletes them on reset).
Server-side revocation#
auth.SessionGuard.Login records the session owner under web.SessionUserKey as "<guard>:<user id>" (guard.SessionUserID(id)), and server-side stores that implement session.UserStore (database, memory and Redis) save it with the session. RevokeOthers deletes the records at once:
m, _ := app.Sessions()
owner := webGuard.SessionUserID(user.AuthID())
err := m.RevokeOthers(ctx, owner, c.Session().ID()) // "" as keepID ends them all- Encrypted cookie sessions (
SESSION_DRIVER=cookie, the default) and stores without user tracking returnsession.ErrRevokeUnsupported: there is no server-side record to delete. Use the session version for them. - A request already in flight when its session is revoked does not write it back: existing sessions are updated, never re-inserted (
UserStore.UpdatereturnsErrSessionNotFound), and its cookie is cleared. - One session per owner: when a session logs in with several guards, the last login owns it; a session whose recorded guard has logged out is taken over by the guard still logged in on its next request.
Adopting session versions in an existing app#
Apps generated by install:auth before v0.25 have no users.session_version column. Before adding AuthSessionVersion to the user model, add a migration:
s.Table("users", func(t *schema.Table) { t.BigInteger("session_version").Default(0) })then bump it where credentials change (copy bumpSessionVersion, the remember_tokens cleanup and the RefreshSession call from a freshly generated app). Existing sessions and remember-me tokens count as version 0 and keep working until the first bump.
Upgrading from v0.23 or earlier#
- At boot
CreateTablerunsALTER TABLE sessions ADD COLUMN user_idandCREATE INDEX sessions_user_id: the database user needs those permissions. Servers booting together do not fail on the race. - Rows keep their clear id until their first load re-keys them to SHA-256, or until they expire. A miss on the hashed key costs a second lookup by clear id (two queries for unknown or expired cookies).
- Sessions logged in before the upgrade carry no owner:
RevokeOthersdoes not reach them until the owner's next authenticated request, when the guard backfillsweb.SessionUserKey; idle ones expire withSESSION_LIFETIME.
Listing a user's sessions#
Stores that implement session.ListStore (database, memory and Redis) list the sessions of an owner for an "active sessions" page, and end one of them:
infos, err := m.UserSessions(ctx, owner, c.Session().ID()) // most recently used first
for _, s := range infos {
// s.Handle, s.CreatedAt, s.LastSeenAt (to the minute), s.ExpiresAt, s.UserAgent, s.IP, s.Current
}
err = m.RevokeSession(ctx, owner, handle) // from a form: ErrSessionNotFound unless owner has that session- The handle is the first 16 hex digits of a hash of the stored key: it can be shown in a page and posted back, but it neither loads a session nor names a row of the sessions table.
RevokeSessiononly ends sessions of the owner given, so take the owner from the logged-in user, never from the form. UserAgentandIPare empty unlessSESSION_RECORD_CLIENT=true(session.Config.RecordClient): both are personal data. When on, they are recorded when a logged-in session is created (the login), the user agent capped at 256 bytes and the IP truncated to its network (IPv4/24, IPv6/48), fromc.IP()(soTRUSTED_PROXIESapplies); control and format runes (escape sequences, bidi overrides) are removed from the user agent. It is still client input: render it escaped (html/templatedoes). Mention both in your privacy notice.- Cookie sessions return
session.ErrListUnsupported: the server never sees the other browsers' cookies. - On Redis,
UserSessions,RevokeSessionandRevokeOthersread each of the user's sessions with a few commands (three per session, one round trip each): fine for the handful of sessions a user has, and kept unpipelined so every command touches one key on Redis Cluster.
Validation errors and old input#
When a form post fails validation, the session's validation hook flashes the field errors and the submitted input, then redirects back. On the next request:
c.Errors() // map[string][]string
c.Old("title") // previous value; dotted paths for nested fields ("author.email")In views use {{error "title"}}, {{errors}} and {{old "title"}}. Password fields, _token and _method are never flashed.
Behaviour#
- Lazy: nothing is read or written unless the handler uses the session; an untouched session writes no cookie. A new visitor whose session is only read (health probes, anonymous pages, auth or CSRF checks) also gets no cookie and no store row: the session is created on the first write (
Put,Flash, login). - Idle and absolute timeouts; the idle timestamp is refreshed at most once a minute.
- Cookies are
HttpOnly,SameSite=Lax,SecureperSESSION_SECURE. Cookie session data over 4 KB is not saved and is logged as an error (session.ErrCookieTooLarge); switch to a server-side store.