Skip to main content
Version: 1.33.0

Sessions & passwords

The basics for a login form: where sessions live, what the cookie defaults are, and how passwords are hashed. The complete login walkthrough is Your first login.

Sessions​

The session manager is store-pluggable:

StoreWhen to use
memoryDevelopment, single-process tests.
sqlSingle-binary production. Sessions live in your primary DB.
redisMulti-replica or container deployments.
session_store: redis
session_cookie_secure: true # default: true — secure by default
session_cookie_samesite: lax
session_lifetime: 24h
redis_url: redis://localhost:6379

session_cookie_secure defaults to true. The session cookie will not ride over plain HTTP unless you opt out explicitly. Local development over http://localhost must set session_cookie_secure: false in nucleus.yml (or NUCLEUS_SESSION_COOKIE_SECURE=false in the environment) — browsers reject Secure cookies on non-HTTPS origins. Production deployments should never set this to false.

Each session record is enriched with runtime metadata — pod, host, instance — for attribution in audit logs and observability tooling.

Session lifecycle — what the framework does for you​

The framework mounts the session middleware globally at startup. By the time a request reaches your handler, its session is already loaded from the store, and it is saved again after the handler returns.

Do not mount the session middleware a second time. Doing so wraps the session twice and produces double-commit errors.

For simple key/value use, no extra wiring is needed: handlers read and write session values straight through the request context, using the helpers on *router.Context such as c.SessionPutString and c.SessionGetString.

Some operations go beyond get/put — RenewToken after a successful login (the defence against session fixation), Destroy/Invalidate on logout, and flash messaging. For those, capture the session manager once in OnStart via rt.Session() and call it directly:

var authModule = nucleus.Module[struct{}]{
Name: "auth",
Prefix: "/auth",
OnStart: func(ctx context.Context, rt nucleus.Runtime, _ struct{}) error {
sm = rt.Session() // *auth.SessionManager; nil only if session is unconfigured
az = rt.Authorizer() // *authz.Enforcer
return nil
},
Routes: func(r nucleus.Router, _ struct{}) {
r.Post("/login", loginHandler)
r.Post("/logout", logoutHandler)
},
}

// loginHandler: validate credentials, then renew the session token.
func loginHandler(c *nucleus.Context) error {
// ... verify user credentials ...
if err := sm.RenewToken(c.Request.Context()); err != nil {
return err
}
c.SessionPutString("user_id", user.ID)
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
}

// logoutHandler: destroy the session entirely.
func logoutHandler(c *nucleus.Context) error {
return sm.Destroy(c.Request.Context())
}

Password hashing​

Passwords are hashed with bcrypt at cost 12. Each hash embeds the cost it was produced with, so raising the cost in a future release does not invalidate existing hashes — they keep verifying at their original cost.

Re-hashing a stored password at a higher cost is your application's job: do it after a successful CheckPassword, by calling HashPassword again and saving the new value.

import "github.com/jcsvwinston/nucleus/pkg/auth"

hash, err := auth.HashPassword("hunter2")
if err != nil {
// handle hashing failure
}
ok := auth.CheckPassword("hunter2", hash) // (plaintext, hash) → bool

CSRF, CORS and rate limiting​

These are middleware-level concerns, documented in Concepts → Routing & middleware. In short: CORS denies unknown origins by default, and rate limiting is configured from nucleus.yml.

CSRF protection is configured per scaffold: the mvc template ships csrf_enabled: true, the api template leaves it off. Session-mutating routes such as login and logout are exactly the ones it protects — see the quickstart's CSRF note and Your first login for how the token reaches forms and JSON clients.

Sessions per device​

ActiveSessions returns every session the store holds, and Revoke ends one by its token — which together are the "signed in on these devices, sign this one out" screen:

current := sm.Token(ctx)
sessions, err := sm.ActiveSessions(ctx) // for the list
err = sm.Revoke(ctx, someToken) // for one row's button

"Sign out everywhere except here" is one call:

n, err := sm.RevokeWhere(ctx, func(s auth.SessionInfo) bool {
return s.Values["user_id"] == userID && s.Token != current
})

A predicate rather than a user id, because the framework does not own the key your application stores identity under.

What each row can show comes from RuntimeMetadataMiddleware, which records the client address, first-seen and last-seen, the node that served the request, and the user agent:

mux.Use(auth.RuntimeMetadataMiddleware(sm, auth.DetectSessionRuntimeIdentity(), 30*time.Second))

The user agent is attacker-controlled text: it is stripped of control characters and capped before it reaches the session payload. The middleware skips requests with no committed session, so anonymous traffic does not create rows.

caution

SessionInfo carries live secrets — Token is a bearer credential and Values is the whole decoded session. It is for a trusted operator surface, never for an untrusted response or a log.