Documentation
¶
Overview ¶
Package session keeps server-side sessions for every provider of the module and authenticates requests with them.
A Store issues sessions: the cookie carries 32 random bytes only, the Backend sees the SHA-256 of them and AES-256-GCM sealed values. Providers save the authenticated auth.Identity in the session; a Guard turns a request with such a session into a request with the identity in its context, or refuses it (401 JSON for APIs, 302 to a login URL for UIs), and re-validates identities through Refreshers at an interval. An Impersonator lets an identity act as another one in its own session, for a bounded time; it is the only way to start or stop an impersonation.
The package works on net/http and context.Context only, never logs and never starts goroutines; expiry is lazy. Every component is safe for concurrent use after construction.
Index ¶
- Variables
- func ParseKey(encoded string) ([]byte, error)
- type Backend
- type Guard
- type GuardOptions
- type ImpersonationLoader
- type Impersonator
- type ImpersonatorOptions
- type MemoryBackend
- func (b *MemoryBackend) CompareAndSwap(_ context.Context, sessionID, key string, previous, value []byte) (bool, error)
- func (b *MemoryBackend) Get(_ context.Context, sessionID, key string) ([]byte, bool, error)
- func (b *MemoryBackend) Pop(_ context.Context, sessionID, key string) ([]byte, bool, error)
- func (b *MemoryBackend) Put(_ context.Context, sessionID, key string, value []byte, ttl time.Duration) error
- func (b *MemoryBackend) Revoke(_ context.Context, sessionID string, ttl time.Duration) error
- type MemoryBackendOptions
- type Refresher
- type Revoker
- type Session
- func (s Session) Get(ctx context.Context, key string) ([]byte, bool, error)
- func (s Session) Identity(ctx context.Context) (*auth.Identity, error)
- func (s Session) Pop(ctx context.Context, key string) ([]byte, bool, error)
- func (s Session) Put(ctx context.Context, key string, value []byte, ttl time.Duration) error
- func (s Session) SaveIdentity(ctx context.Context, identity *auth.Identity) error
- func (s Session) SwapIdentity(ctx context.Context, previous, identity *auth.Identity) (bool, error)
- type Store
- func (s *Store) End(ctx context.Context, writer http.ResponseWriter, request *http.Request) error
- func (s *Store) MaximumAge() time.Duration
- func (s *Store) Open(request *http.Request) (Session, error)
- func (s *Store) SameSite() http.SameSite
- func (s *Store) Start(ctx context.Context, writer http.ResponseWriter, request *http.Request) (Session, error)
- type StoreOptions
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrRevoked = errors.New("session revoked")
ErrRevoked is returned by Backend.Put for a session that carries a revocation marker. Store and Session translate it into an auth.ErrUnauthenticated refusal.
Functions ¶
Types ¶
type Backend ¶
type Backend interface {
// Put stores value under (sessionID, key) for ttl, replacing an existing value; ErrRevoked when revoked.
Put(ctx context.Context, sessionID, key string, value []byte, ttl time.Duration) error
// Get returns the live value under (sessionID, key); found is false when there is none or it expired.
Get(ctx context.Context, sessionID, key string) (value []byte, found bool, err error)
// Pop returns and deletes the live value in one step: of two concurrent Pops at most one finds the value.
Pop(ctx context.Context, sessionID, key string) (value []byte, found bool, err error)
// CompareAndSwap replaces the live value under (sessionID, key) with value in one step, keeping its expiry,
// only when it still equals previous; swapped is false when it changed, expired or was revoked meanwhile.
CompareAndSwap(ctx context.Context, sessionID, key string, previous, value []byte) (swapped bool, err error)
// Revoke deletes every key of sessionID. When there was any, it writes a marker living ttl, or until the
// latest of the deleted values would have expired when that is sooner.
Revoke(ctx context.Context, sessionID string, ttl time.Duration) error
}
Backend stores the server-side half of sessions: one opaque, already encrypted value per (sessionID, key). sessionID is a non-secret identifier derived from the cookie (SHA-256), never the cookie value. Every operation is atomic per key; Put is serialized against Revoke of the same sessionID, so after Revoke returned no Put for that sessionID succeeds until the marker expires or a full backend evicts it (MemoryBackend; Redis under its own eviction policy). A marker never outlives the values it replaced, and a sessionID without values gets none: revoking unknown IDs stores nothing. Values expire after their own ttl, which is always positive. Applications may implement it (e.g. on Postgres); share one backend between replicas. Implementations must be safe for concurrent use.
type Guard ¶
type Guard struct {
// contains filtered or unexported fields
}
Guard authenticates requests by their session: it puts the stored identity into the request context or refuses the request. An impersonation whose MaximumDuration is up ends on the next request, which then carries the impersonator's identity. One Guard serves every provider. It is safe for concurrent use.
Example ¶
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/govengers/auth"
"github.com/govengers/auth/session"
)
// newExampleStore builds a store on the in-process backend; production code shares a Redis backend
// between replicas and reads the key from its configuration.
func newExampleStore() (*session.Store, error) {
backend, err := session.NewMemoryBackend(nil)
if err != nil {
return nil, err
}
key, err := session.ParseKey("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
if err != nil {
return nil, err
}
return session.NewStore(&session.StoreOptions{Backend: backend, EncryptionKey: key})
}
// loginHandler stands in for a provider: it starts a fresh session and saves the identity in it.
func loginHandler(store *session.Store) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
current, err := store.Start(request.Context(), writer, request)
if err != nil {
auth.WriteError(writer, err)
return
}
identity := &auth.Identity{Provider: "password", Subject: "user-1", Username: "ada", Active: true}
if err := current.SaveIdentity(request.Context(), identity); err != nil {
auth.WriteError(writer, err)
return
}
writer.WriteHeader(http.StatusNoContent)
})
}
// withCookies returns a GET request to target carrying the cookies response set, as a browser would.
func withCookies(target string, response *httptest.ResponseRecorder) *http.Request {
request := httptest.NewRequestWithContext(context.Background(), http.MethodGet, target, nil)
for _, line := range response.Header().Values("Set-Cookie") {
if cookie, err := http.ParseSetCookie(line); err == nil {
request.AddCookie(cookie)
}
}
return request
}
func main() {
store, err := newExampleStore()
if err != nil {
fmt.Println(err)
return
}
guard, err := session.NewGuard(&session.GuardOptions{Store: store, LoginURL: "/login"})
if err != nil {
fmt.Println(err)
return
}
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()))
anonymousAPI := httptest.NewRecorder()
serveMux.ServeHTTP(anonymousAPI, httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/api/me", nil))
anonymousUI := httptest.NewRecorder()
serveMux.ServeHTTP(anonymousUI, httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/reports?year=2026", nil))
login := httptest.NewRecorder()
serveMux.ServeHTTP(login, httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/login", nil))
authenticated := httptest.NewRecorder()
serveMux.ServeHTTP(authenticated, withCookies("/api/me", login))
fmt.Print(anonymousAPI.Code, " ", anonymousAPI.Body.String())
fmt.Println(anonymousUI.Code, anonymousUI.Header().Get("Location"))
fmt.Print(authenticated.Code, " ", authenticated.Body.String())
}
Output: 401 {"code":"unauthenticated","message":"authentication required"} 302 /login?return_to=%2Freports%3Fyear%3D2026 200 {"provider":"password","subject":"user-1","username":"ada","active":true,"roles":[]}
func NewGuard ¶
func NewGuard(options *GuardOptions) (*Guard, error)
NewGuard validates options and returns a Guard working on a copy of them.
func (*Guard) RequireAPI ¶
RequireAPI refuses unauthenticated requests with 401 JSON, forbidden ones with 403 JSON. A request that already carries an identity (e.g. from an API key) passes untouched.
type GuardOptions ¶
type GuardOptions struct {
Store *Store // required
LoginURL string // RequireUI target; absolute http(s) URL or path starting with "/", without a fragment; default "/"
ReturnPathHeader string // header carrying the original request URI (forward auth: "X-Forwarded-Uri"); default: request.URL.RequestURI()
Refreshers []Refresher // optional; names must be unique
RefreshInterval time.Duration // default 1 min
// MaximumUnverifiedAge bounds a degraded refresh: a refresh failing for longer than this since the login
// or the last successful refresh ends the session. Default 1 h (RefreshInterval when that is longer); at
// least RefreshInterval.
MaximumUnverifiedAge time.Duration
Observer auth.Observer // optional
}
GuardOptions configures NewGuard.
type ImpersonationLoader ¶ added in v1.1.0
ImpersonationLoader returns the identity of the user subject names, as a login of that user would produce it (OnLogin applied), or nil without an error when there is none. ctx carries the impersonator's identity (auth.IdentityFrom), so a loader can refuse a target with an *auth.Error, e.g. one of another tenant. It must be safe for concurrent use.
type Impersonator ¶ added in v1.1.0
type Impersonator struct {
// contains filtered or unexported fields
}
Impersonator lets an identity act as another one in its own session. It is the only way to start or stop an impersonation: SaveIdentity and SwapIdentity refuse to. An impersonation needs an identity that passes Actor and is not impersonating already, and a target that Load returns, that passes Target, is active and is not the impersonator. It keeps the session (its cookie, lifetime and provider values) and ends when stopped, with the session, or MaximumDuration after its start, whichever comes first. It is safe for concurrent use.
Example ¶
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"github.com/govengers/auth"
"github.com/govengers/auth/session"
)
// newExampleStore builds a store on the in-process backend; production code shares a Redis backend
// between replicas and reads the key from its configuration.
func newExampleStore() (*session.Store, error) {
backend, err := session.NewMemoryBackend(nil)
if err != nil {
return nil, err
}
key, err := session.ParseKey("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
if err != nil {
return nil, err
}
return session.NewStore(&session.StoreOptions{Backend: backend, EncryptionKey: key})
}
// withCookies returns a GET request to target carrying the cookies response set, as a browser would.
func withCookies(target string, response *httptest.ResponseRecorder) *http.Request {
request := httptest.NewRequestWithContext(context.Background(), http.MethodGet, target, nil)
for _, line := range response.Header().Values("Set-Cookie") {
if cookie, err := http.ParseSetCookie(line); err == nil {
request.AddCookie(cookie)
}
}
return request
}
// adminLoginHandler stands in for a provider whose OnLogin grants the admin role.
func adminLoginHandler(store *session.Store) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
current, err := store.Start(request.Context(), writer, request)
if err != nil {
auth.WriteError(writer, err)
return
}
admin := &auth.Identity{Provider: "password", Subject: "admin-1", Username: "grace", Active: true, Roles: []string{auth.RoleAdmin}}
if err := current.SaveIdentity(request.Context(), admin); err != nil {
auth.WriteError(writer, err)
return
}
writer.WriteHeader(http.StatusNoContent)
})
}
func main() {
store, err := newExampleStore()
if err != nil {
fmt.Println(err)
return
}
guard, err := session.NewGuard(&session.GuardOptions{Store: store})
if err != nil {
fmt.Println(err)
return
}
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
login := httptest.NewRecorder()
serveMux.ServeHTTP(login, httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/login", nil))
start := withCookies("/auth/impersonation", login)
start.Method, start.Body = http.MethodPost, io.NopCloser(strings.NewReader(`{"subject":"user-2"}`))
start.Header.Set("Content-Type", "application/json")
serveMux.ServeHTTP(httptest.NewRecorder(), start)
impersonating := httptest.NewRecorder()
serveMux.ServeHTTP(impersonating, withCookies("/api/me", login))
fmt.Print(impersonating.Body.String())
}
Output: {"provider":"password","subject":"user-2","username":"ada","active":true,"roles":[],"impersonator":{"provider":"password","subject":"admin-1","username":"grace","active":true,"roles":["admin"]}}
func NewImpersonator ¶ added in v1.1.0
func NewImpersonator(options *ImpersonatorOptions) (*Impersonator, error)
NewImpersonator validates options and returns an Impersonator working on a copy of them.
func (*Impersonator) Routes ¶ added in v1.1.0
func (i *Impersonator) Routes() []auth.Route
Routes returns POST {PathPrefix} (JSON body {"subject": "..."}: Start, answers 200 with the impersonating identity's auth.IdentityResponse) and DELETE {PathPrefix} (Stop, answers 200 with the impersonator's), both behind the Guard's RequireAPI. Register them with auth.RegisterRoutes or ginadapter.RegisterRoutes. The POST requires Content-Type application/json and DELETE is no simple request, so a cross-site page cannot send either without a CORS preflight.
func (*Impersonator) Start ¶ added in v1.1.0
Start makes the request's identity (auth.IdentityFrom, put there by the Guard) impersonate the identity Load returns for subject, in the request's session, and returns the impersonating identity. Refusals: no identity 401; already impersonating 409 already_impersonating; inactive 403; an empty subject 400; Actor's or Load's refusal; no such target 400 unknown_impersonation_target; the impersonator itself 400 self_impersonation; an inactive target 403; Target's refusal; a session that no longer holds the request's identity 409. It reports the outcome (Action impersonation_start) under the impersonator's provider.
func (*Impersonator) Stop ¶ added in v1.1.0
Stop ends the impersonation of the request's identity (auth.IdentityFrom) in the request's session and returns the impersonator's identity as the session now holds it. Refusals: no identity 401; not impersonating 409 not_impersonating; a session that no longer holds the request's identity 409. It reports the outcome (Action impersonation_stop) under the impersonator's provider.
type ImpersonatorOptions ¶ added in v1.1.0
type ImpersonatorOptions struct {
Guard *Guard // required; its Store holds the sessions, and Routes run behind its RequireAPI
Load ImpersonationLoader // required
// Actor decides who may impersonate, evaluated against the impersonator's identity; default
// authorization.IsAdmin().
Actor authorization.Check
// Target decides who may be impersonated, evaluated against the target's identity; default
// authorization.Not(authorization.IsAdmin()), so nobody gains an admin's rights by impersonating one.
Target authorization.Check
MaximumDuration time.Duration // default 1 h, at least 1 s; the Guard ends an impersonation that long after its start
PathPrefix string // default "/auth/impersonation"; segments of A-Z a-z 0-9 . _ ~ -, no trailing "/"
Observer auth.Observer // optional
}
ImpersonatorOptions configures NewImpersonator.
type MemoryBackend ¶
type MemoryBackend struct {
// contains filtered or unexported fields
}
MemoryBackend implements Backend with a map and one mutex; expiry is lazy (no goroutine). It is correct for exactly one replica and safe for concurrent use. A full backend evicts revocation markers like sessions, so a flood of login starts cannot block every new session; the cost is that an evicted marker no longer refuses a Put for its revoked session. Nothing can give that session an identity again (SaveIdentity and the Guard swap against a live identity, and only the first save of a Start handle writes without one), but a provider or application value may land in it. Meant for one replica at non-production scale; use Redis in production.
func NewMemoryBackend ¶
func NewMemoryBackend(options *MemoryBackendOptions) (*MemoryBackend, error)
NewMemoryBackend returns the in-process backend: correct for exactly one replica. nil options allowed.
func (*MemoryBackend) CompareAndSwap ¶
func (b *MemoryBackend) CompareAndSwap(_ context.Context, sessionID, key string, previous, value []byte) (bool, error)
CompareAndSwap replaces the live value under (sessionID, key) with a copy of value, keeping its expiry, when it equals previous.
func (*MemoryBackend) Put ¶
func (b *MemoryBackend) Put(_ context.Context, sessionID, key string, value []byte, ttl time.Duration) error
Put stores a copy of value under (sessionID, key) for ttl; ErrRevoked when sessionID is revoked.
type MemoryBackendOptions ¶
type MemoryBackendOptions struct {
// MaximumSessions defaults to 100000, MaximumBytes (session IDs, keys and values held) to 64 MiB. When a
// write would pass either (a replacement counts only what it adds), expired entries are dropped, then the entries expiring soonest are evicted, a
// live revocation marker included. A single value that cannot fit is refused.
MaximumSessions int
MaximumBytes int
}
MemoryBackendOptions configures NewMemoryBackend.
type Refresher ¶
type Refresher interface {
Name() string // the Identity.Provider it refreshes
Refresh(ctx context.Context, current Session, identity *auth.Identity) (*auth.Identity, error)
}
Refresher re-validates identities of one provider. The Guard calls it for identities whose last refresh is older than RefreshInterval, always with the real identity (identity.Real()). Return a copy with provider-owned fields updated; keep every other field (roles set by OnLogin etc.). ErrUnauthenticated or ErrForbidden end the session (a Refresher that is also a Revoker is called first); any other error keeps the identity (degraded) until the last successful verification is older than GuardOptions.MaximumUnverifiedAge. Refresh runs without the request's cancellation, bounded by 30 s. Implementations must be safe for concurrent use.
type Revoker ¶ added in v1.1.0
Revoker is optionally implemented by a Refresher whose provider holds state for a session at the identity provider, such as GitHub's OAuth token. Before the Guard ends a session whose refresh it refused (a refusal of the Refresher, or a failure lasting longer than MaximumUnverifiedAge), it calls RevokeSession with the session, which still holds the provider's values, on a context without the request's cancellation and with its own 10 s timeout. An error only degrades the outcome: the session ends either way. A session that expires (MaximumAge) or ends otherwise is not seen by the Guard, so it is not called then. Implementations must be safe for concurrent use.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is a handle on one server-side session (value type, unexported fields: the backend ID and the store). Values are AES-256-GCM sealed under a key derived per session from the store's key (HKDF-SHA256), with a random 12-byte nonce and additional data "<sessionID>\x00<key>"; a value that does not decrypt (moved, tampered, other key) counts as absent. A Session is safe for concurrent use; the zero value is not usable. The name repeats the package on purpose (NAM-003): it is the package's domain noun, like time.Time, and session.Handle would hide what it is a handle on.
func (Session) Get ¶
Get returns the value under key; false when there is none, it expired or it does not decrypt.
func (Session) Identity ¶
Identity returns the stored identity; none, expired (now > authenticatedAt + MaximumAge) or revoked: *auth.Error ErrUnauthenticated.
func (Session) Pop ¶
Pop returns the value under key and deletes it; of two concurrent Pops at most one finds it. The key "identity" is reserved, as for Put.
func (Session) Put ¶
Put seals value and stores it under key for ttl (positive). A revoked session refuses with ErrUnauthenticated. The key "identity" is reserved: only SaveIdentity and SwapIdentity write it, with their lifetime and swap rules.
func (Session) SaveIdentity ¶
SaveIdentity stores identity under key "identity". The first save of a session sets authenticatedAt to the time Start issued it; later saves (TOTP enable, application changes) keep it and refreshedAt: they verify nothing, so only the login and a Guard refresh may postpone the next refresh. TTL = authenticatedAt + MaximumAge - now. Only a session returned by Start may have a first save, and only one: without a live identity (never authenticated, expired, revoked) any other save refuses with ErrUnauthenticated, so neither a planted cookie, a save racing the expiry nor a retained Start handle gets a fresh lifetime. A later save swaps against the identity it read, so a save racing a logout cannot write the identity back even where the backend forgot the revocation (a full MemoryBackend). A save never changes who the session belongs to: the first one refuses an impersonating identity, a later one an identity or impersonator of another provider or subject than the stored ones (see Impersonator).
func (Session) SwapIdentity ¶
SwapIdentity saves identity in place of previous (an identity Identity returned) only while the session still holds an identity equal to it (same JSON); swapped is false when another request saved a different one meanwhile or changed the session between the read and the swap. It keeps authenticatedAt, the expiry and refreshedAt, like SaveIdentity. A session without a live identity refuses with ErrUnauthenticated, like Identity. Like SaveIdentity it never changes who the session belongs to: an identity or impersonator of another provider or subject than the stored ones is an error (see Impersonator).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store issues and ends server-side sessions. The cookie carries 32 random bytes; the backend sees their SHA-256 and sealed values only. It is safe for concurrent use.
Example ¶
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/govengers/auth"
"github.com/govengers/auth/session"
)
// newExampleStore builds a store on the in-process backend; production code shares a Redis backend
// between replicas and reads the key from its configuration.
func newExampleStore() (*session.Store, error) {
backend, err := session.NewMemoryBackend(nil)
if err != nil {
return nil, err
}
key, err := session.ParseKey("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
if err != nil {
return nil, err
}
return session.NewStore(&session.StoreOptions{Backend: backend, EncryptionKey: key})
}
// loginHandler stands in for a provider: it starts a fresh session and saves the identity in it.
func loginHandler(store *session.Store) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
current, err := store.Start(request.Context(), writer, request)
if err != nil {
auth.WriteError(writer, err)
return
}
identity := &auth.Identity{Provider: "password", Subject: "user-1", Username: "ada", Active: true}
if err := current.SaveIdentity(request.Context(), identity); err != nil {
auth.WriteError(writer, err)
return
}
writer.WriteHeader(http.StatusNoContent)
})
}
// withCookies returns a GET request to target carrying the cookies response set, as a browser would.
func withCookies(target string, response *httptest.ResponseRecorder) *http.Request {
request := httptest.NewRequestWithContext(context.Background(), http.MethodGet, target, nil)
for _, line := range response.Header().Values("Set-Cookie") {
if cookie, err := http.ParseSetCookie(line); err == nil {
request.AddCookie(cookie)
}
}
return request
}
func main() {
store, err := newExampleStore()
if err != nil {
fmt.Println(err)
return
}
login := httptest.NewRecorder()
loginHandler(store).ServeHTTP(login, httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/login", nil))
current, err := store.Open(withCookies("/", login))
if err != nil {
fmt.Println(err)
return
}
identity, err := current.Identity(context.Background())
if err != nil {
fmt.Println(err)
return
}
fmt.Println(identity.Provider, identity.Username)
}
Output: password ada
func NewStore ¶
func NewStore(options *StoreOptions) (*Store, error)
NewStore validates options and returns a Store working on a copy of them. A Secure, host-only cookie on path "/" is named "__Host-" + CookieName, unless CookieName has its own "__Host-" or "__Secure-" prefix: browsers accept such a cookie only from this host over https, so a sibling subdomain cannot toss a cookie of the same name that the store would read first. A "__Host-" CookieName needs exactly those attributes, a "__Secure-" one a Secure cookie.
func (*Store) End ¶
End revokes the request's session (with context.WithoutCancel and its own 10 s timeout, so a disconnecting client cannot keep it and a hung backend cannot hold the request) and expires the cookie. No cookie: only expires it.
func (*Store) MaximumAge ¶
MaximumAge returns the configured absolute session lifetime.
func (*Store) Open ¶
Open returns the handle of the request's session from its cookie (no I/O). No or malformed cookie: *auth.Error ErrUnauthenticated.
func (*Store) SameSite ¶
SameSite returns the cookie's SameSite mode. The OIDC and GitHub providers refuse a store with http.SameSiteStrictMode: the browser withholds a Strict cookie on the redirect back from the provider, and the login state with it.
func (*Store) Start ¶
func (s *Store) Start(ctx context.Context, writer http.ResponseWriter, request *http.Request) (Session, error)
Start issues a new session: revokes the request's current session (if any), sets a fresh cookie, returns the handle. Used at login start and again after authentication (session ID rotation, fixation defense).
type StoreOptions ¶
type StoreOptions struct {
Backend Backend // required
EncryptionKey []byte // required, exactly 32 bytes (AES-256-GCM), not one byte repeated; see ParseKey
CookieName string // default "auth_session"; "__Host-" is prepended when Secure, host-only and on "/" (see NewStore)
CookieDomain string // default "" (host-only cookie)
CookiePath string // default "/"
SameSite http.SameSite // Lax (default), Strict or None; None requires InsecureCookie false (front-channel logout needs None); Strict only for password-only stores (oidc and github refuse it); SameSiteDefaultMode would send no attribute and is refused
InsecureCookie bool // drops the Secure attribute; local HTTP development only
MaximumAge time.Duration // default 24h, at least 1s; absolute session lifetime, also the cookie Max-Age
}
StoreOptions configures NewStore.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package redisbackend implements session.Backend on Redis, so that sessions work across replicas.
|
Package redisbackend implements session.Backend on Redis, so that sessions work across replicas. |