Repository navigation
Sessions
session.Store issues server-side sessions (the cookie carries 32 random bytes; values are AES-256-GCM sealed in a Backend). session.Guard authenticates requests by their session for every provider and re-validates identities through Refreshers.
session.NewStore(&session.StoreOptions{...}):
| Option | Default | Note |
|---|---|---|
Backend |
required |
session.NewMemoryBackend(nil) or redisbackend.New
|
EncryptionKey |
required | exactly 32 bytes, not one byte repeated (an all-zero placeholder is refused): session.ParseKey(os.Getenv(...)), generated with openssl rand -base64 32; same on every replica; changing it logs everyone out (no rotation list) |
CookieName |
auth_session |
sent as __Host-auth_session while the cookie is Secure, host-only and on / (the default) |
CookieDomain, CookiePath
|
host-only, /
|
either one drops the __Host- prefix |
SameSite |
http.SameSiteLaxMode |
None for OIDC front-channel logout (https only); Strict only without OIDC and GitHub providers; SameSiteDefaultMode is refused |
InsecureCookie |
false | drops Secure; local plain-http development only |
MaximumAge |
24 h | absolute lifetime from the login, never extended by activity or refresh |
Application code rarely touches the store directly: store.Open(request) returns the session.Session handle (for github.Provider.HTTPClient or own values via Put, Get, Pop; the key identity is reserved). Session.SwapIdentity saves a changed identity only while the session still holds the one you read (own role changes). Neither save changes who the session belongs to: both refuse an identity or impersonator of another provider or subject than the stored ones, so only a login replaces the user and only the Impersonator starts or stops an impersonation.
session.NewGuard(&session.GuardOptions{...}), one per application, shared by every provider:
-
RequireAPI()answers 401 JSON without a session, 403 JSON for a refused identity. -
RequireUI()answers 302 toLoginURL?return_to=<path>without a session; 403 JSON for a refused identity (a redirect would loop). - Both pass a request that already carries an identity (from
apikeymounted in front) untouched. -
Identify(ctx, request)is the same lookup without a response: it returns the identity of the request's session or the refusal the middleware would answer with (auth.ErrUnauthenticatedwithout a valid session, the Refresher's refusal). For a caller that has no response writer any more, such as a WebSocket re-validating the cookies of its upgrade request on every message or a timer. It ends an expired impersonation and runs a due refresh exactly like the middleware (store writes only); a refused refresh ends the session in the store, and only the cookie stays with the client, naming a revoked session until a login or logout replaces it. An identity already inctxis not consulted, and the lookup runs onctx, not on the request's context (an upgrade request's context is done once its handler returned).
| Option | Default | Note |
|---|---|---|
Store |
required | |
LoginURL |
/ |
provider.LoginURL() for OIDC and GitHub; your login page for password |
ReturnPathHeader |
request URI | forward auth uses X-Forwarded-Uri
|
Refreshers |
none | password and GitHub providers; names must be unique |
RefreshInterval |
1 min | |
MaximumUnverifiedAge |
1 h | how long a failing (not refusing) refresh keeps the identity |
Tested excerpt of a Guard in front of API and UI routes (session/example_test.go):
serveMux := http.NewServeMux()
serveMux.Handle("POST /login", loginHandler(store))
serveMux.Handle("GET /api/me", guard.RequireAPI()(auth.IdentityHandler()))
serveMux.Handle("GET /reports", guard.RequireUI()(http.NotFoundHandler()))The Guard calls a provider's Refresher for identities whose last refresh is older than RefreshInterval, once per interval per session, detached from the request (30 s bound). Only the login and the Guard's own refresh count: SaveIdentity and SwapIdentity verify nothing and keep the last refresh time, so frequent application saves cannot postpone the Refresher or the MaximumUnverifiedAge cutoff. auth.ErrUnauthenticated or auth.ErrForbidden from it ends the session; any other error keeps the identity until MaximumUnverifiedAge since the last successful check, then ends it. Before ending such a session the Guard calls RevokeSession when the Refresher is also a session.Revoker (GitHub revokes its OAuth token), detached from the request with its own 10 s; its error only degrades the outcome, the session ends either way. Expiry (MaximumAge) and Store.Start rotation call no provider. A refresh never overwrites a change saved while it ran. A backend that fails the refresh's claim (its first write) gets a 500 without a provider call, unless reading the session back shows the claim committed.
-
password: re-reads the user; a deleted user loses the session, and so does a session opened before the user's password hash or second factor changed (Password). UpdatesUsernameandEmailonly. -
github: refetches teams; outside every allowed team or token revoked ends the session; admin role followsAdminTeams. -
oidc: no refresher; roles and groups are fixed at login.
session.NewImpersonator(&session.ImpersonatorOptions{...}) lets an identity act as another one in its own session, for support and admin tools. It is the only way to start or stop an impersonation.
| Option | Default | Note |
|---|---|---|
Guard |
required | its Store holds the sessions; the routes run behind its RequireAPI
|
Load |
required |
func(ctx, subject) (*auth.Identity, error): the target as a login of that user would produce it (your user table, your OnLogin); nil, nil when there is no such user; ctx carries the impersonator, so return an *auth.Error to refuse a target (another tenant) |
Actor |
authorization.IsAdmin() |
who may impersonate |
Target |
authorization.Not(authorization.IsAdmin()) |
who may be impersonated; nobody gains an admin's rights by impersonating one |
MaximumDuration |
1 h | the Guard ends the impersonation that long after its start, at least 1 s |
PathPrefix |
/auth/impersonation |
|
Observer |
none |
impersonation_start and impersonation_stop outcomes under the impersonator's provider |
Routes(): POST {PathPrefix} with {"subject":"..."} starts (200, the impersonating identity's IdentityResponse with impersonator set), DELETE {PathPrefix} stops (200, the impersonator's). Both run behind the Guard's RequireAPI; the JSON body and the DELETE method need a CORS preflight, so a cross-site page cannot send them. Your own handler behind the Guard calls Start(request, subject) and Stop(request) instead.
Whatever the options say, it refuses: an impersonating identity (409 already_impersonating), an inactive impersonator (403), an empty subject (400), no target (400 unknown_impersonation_target), the impersonator itself, by provider and subject or by UserID (400 self_impersonation), an inactive target (403), a session saved by another request since the Guard read it (409), and Stop without an impersonation (409 not_impersonating). The session keeps its cookie, lifetime and provider values (the GitHub token, the OIDC logout hint); the impersonation ends when stopped, with the session, or after MaximumDuration, whichever comes first, and the next request then carries the impersonator. The impersonated identity is the snapshot Load returned; the Guard keeps refreshing the impersonator, and every authorization check also requires the impersonator to be active. Logout ends the whole session. authorization.Real(check) evaluates the impersonator; jwt.Signer carries it as the RFC 8693 act claim.
Tested (session/example_test.go, ExampleImpersonator):
users := map[string]*auth.Identity{"user-2": {Provider: "password", Subject: "user-2", Username: "ada", Active: true}}
impersonator, err := session.NewImpersonator(&session.ImpersonatorOptions{
Guard: guard,
// Actor defaults to authorization.IsAdmin(), Target to authorization.Not(authorization.IsAdmin()).
Load: func(_ context.Context, subject string) (*auth.Identity, error) {
return users[subject].Clone(), nil // your user table and OnLogin; nil: no such user
},
})
if err != nil {
fmt.Println(err)
return
}
serveMux := http.NewServeMux()
serveMux.Handle("POST /login", adminLoginHandler(store))
serveMux.Handle("GET /api/me", guard.RequireAPI()(auth.IdentityHandler()))
auth.RegisterRoutes(serveMux, impersonator.Routes()) // POST and DELETE /auth/impersonation-
MemoryBackend: exactly one replica, lost on restart, non-production scale. Bounded byMaximumSessions(default 100000) andMaximumBytesof session IDs, keys and values (default 64 MiB; a replacement counts only what it adds): when full it drops expired entries, then evicts the entries expiring soonest, revocation markers included (an expiry-ordered heap: O(log n) per evicted entry, never a scan of every session); a single value larger thanMaximumBytesis refused. It is not built for anonymous login floods: every login start stores a session, so a flood keeps it full and evicts real sessions; rate-limit the login routes or use Redis. -
session/redisbackend: anyredis.UniversalClient(client, sentinel, cluster); you own and close the client; give it aTLSConfigwhen Redis is reached over a network you do not trust (the values are ciphertext, but the password and the commands are not; the forwardauth binary does this withSESSION_REDIS_TLS_*and reaches a Sentinel setup withSESSION_REDIS_SENTINEL_*, see Forward-Auth Configuration); keys carry a hash tag, soKeyPrefix(defaultsession) must not contain{or}. Logout and session rotation revoke on every replica; a revoked session never regains an identity and refuses writes while its revocation marker lives (Redis may evict it under its own eviction policy, as a full memory backend does). A value counts only while its session's index set lists it (the index lives at least as long as every value), because logout finds the values through the index: a session whose index Redis evicted reads as logged out, so its cookie can never outlive a logout. Run the session Redis with amaxmemory-policyof:-
noeviction(recommended, ideally on a Redis of its own): nothing is ever dropped; when memory is full, writes that need memory fail, so logins and saves answer 500 until memory frees up. -
volatile-ttl: every session key has a TTL, the shortest-lived go first (pending logins, one-minute retry receipts, sessions about to expire); an index outlives its values, so values go first. An evicted revocation marker lets the revoked session accept writes again, never an identity. - any
allkeys-*or othervolatile-*policy evicts arbitrary keys: users are logged out at random (an evicted value or index), logins fail (an evicted login state), revocation markers vanish as above; never a resurrected session, but unpredictable.
Tested excerpt (session/redisbackend/example_test.go):
-
client := redis.NewClient(&redis.Options{Addr: server.Addr()})
defer func() { _ = client.Close() }() // nothing to flush
backend, err := redisbackend.New(&redisbackend.Options{Client: client, KeyPrefix: "myapp:session"})
if err != nil {
fmt.Println(err)
return
}
key, err := session.ParseKey("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
if err != nil {
fmt.Println(err)
return
}
store, err := session.NewStore(&session.StoreOptions{Backend: backend, EncryptionKey: key})- Own database: implement the five methods of
session.Backend(Put,Get,Pop,CompareAndSwap,Revoke) with their atomicity contracts. The built-in backends run the conformance suite internal/backendtest; it is internal to the module, so copy it into your tests.
- Instances sharing
EncryptionKey, RedisKeyPrefixand cookie name accept each other's sessions: give every application its own key and prefix. - Plain
httpon a host other thanlocalhost/loopback: browsers drop theSecurecookie and every login loops; setInsecureCookiefor local development only. -
SameSite: http.SameSiteStrictModeis for password-only stores: the browser withholds it on the redirect back from the IdP, sooidc.Newandgithub.Newrefuse such a store. - No idle timeout: a session lives until logout,
MaximumAgeor a refusing refresh.
Source: session/ (store.go, guard.go, session.go, memory.go), session/redisbackend/backend.go.
Mirror of docs/wiki at v1.3.0 (486bfa3). Edit the repository, not this wiki.