Pular para o conteúdo
Esta página ainda não foi traduzida. O conteúdo abaixo está em inglês. Ler em inglês

The basics

Sessions

Encrypted cookie, memory and database session stores with flash messages and old input.

Última atualização:

vukem.DefaultMiddleware installs the session middleware on the router stack; app.Sessions() builds the *session.Manager from SESSION_* variables.

Configuration#

VariableDefaultNotes
SESSION_DRIVERcookiecookie (data encrypted in the cookie), memory or database (the app database); Redis is installed with vukem.WithSessionStore from drivers/redis, which takes precedence
SESSION_LIFETIME24hAbsolute lifetime
SESSION_IDLE_TIMEOUT2hExpires after inactivity
SESSION_COOKIEvukem_sessionCookie name
SESSION_SECUREautoauto = secure in production or when APP_URL is https://
SESSION_RECORD_CLIENTfalseKeep the user agent and client network with each logged-in server-side session, for the active sessions list

Stores#

StorePackageNotes
CookiesessionAES-256-GCM with a key derived from APP_KEY (HKDF); tampered cookies are rejected and logged as session.invalid
Memorysession.NewMemoryStore()Tests and development
DatabaseSESSION_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)
Redisdrivers/redisvukem.WithSessionStore(rc.SessionStore("")); records the owner of each session, so revoking and listing work as on the database store

Usage#

Go
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")
}
MethodMeaning
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:

Go
_, 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 AuthSessionVersion on the value receiver when the guard holds values (SessionGuard[models.User]): NewSessionGuard panics 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; RefreshSession issues 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:

Go
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 return session.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.Update returns ErrSessionNotFound), 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:

Go
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 CreateTable runs ALTER TABLE sessions ADD COLUMN user_id and CREATE 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: RevokeOthers does not reach them until the owner's next authenticated request, when the guard backfills web.SessionUserKey; idle ones expire with SESSION_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:

Go
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. RevokeSession only ends sessions of the owner given, so take the owner from the logged-in user, never from the form.
  • UserAgent and IP are empty unless SESSION_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), from c.IP() (so TRUSTED_PROXIES applies); control and format runes (escape sequences, bidi overrides) are removed from the user agent. It is still client input: render it escaped (html/template does). Mention both in your privacy notice.
  • Cookie sessions return session.ErrListUnsupported: the server never sees the other browsers' cookies.
  • On Redis, UserSessions, RevokeSession and RevokeOthers read 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:

Go
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, Secure per SESSION_SECURE. Cookie session data over 4 KB is not saved and is logged as an error (session.ErrCookieTooLarge); switch to a server-side store.