auth

package
v1.28.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 34 Imported by: 0

Documentation

Overview

Package auth provides authentication utilities for the Nucleus framework, including password hashing, JWT management, and session handling.

Index

Constants

View Source
const (
	// SessionMetaFirstSeenAtKey stores the first server-side observation timestamp (RFC3339).
	SessionMetaFirstSeenAtKey = "__nucleus_first_seen_at"
	// SessionMetaLastSeenAtKey stores the latest server-side observation timestamp (RFC3339).
	SessionMetaLastSeenAtKey = "__nucleus_last_seen_at"
	// SessionMetaPodKey stores the pod identifier handling the session request.
	SessionMetaPodKey = "__nucleus_runtime_pod"
	// SessionMetaHostKey stores the host/node identifier handling the session request.
	SessionMetaHostKey = "__nucleus_runtime_host"
	// SessionMetaInstanceKey stores a composed runtime instance identifier.
	SessionMetaInstanceKey = "__nucleus_runtime_instance"
	// SessionMetaRemoteIPKey stores the latest observed client IP for the session.
	SessionMetaRemoteIPKey = "__nucleus_remote_ip"
	// SessionMetaUserAgentKey stores the user agent the session was last
	// seen from. Without it a device list can say when and from where a
	// session was used, and never from WHAT — which is the column that
	// makes the list actionable ("this Firefox on Windows is not mine").
	SessionMetaUserAgentKey = "__nucleus_user_agent"
)
View Source
const DefaultFederatedPendingTTL = 15 * time.Minute

DefaultFederatedPendingTTL is how long a started sign-in stays valid. Long enough for somebody to type a password and answer a second factor, short enough that an abandoned flow is not a lasting foothold.

Variables

View Source
var (
	// ErrInvalidCredentials reports a certain rejection. See
	// backend.ErrInvalidCredentials.
	ErrInvalidCredentials = backend.ErrInvalidCredentials
	// ErrBackendUnavailable reports that a backend could not reach its
	// source. See backend.ErrBackendUnavailable.
	ErrBackendUnavailable = backend.ErrBackendUnavailable
	// ErrUserNotFound is what a UserProvider returns when the username
	// does not exist. A provider may return it, or any other error; the
	// adapter in user_provider_backend.go maps every lookup failure to the
	// same outcome on purpose.
	//
	// It belongs in this block, with the other two: it used to be declared
	// separately with its own errors.New and the SAME message text, so a
	// leaf backend returning backend.ErrUserNotFound was unrecognisable to
	// code comparing against this one — and identical messages left nothing
	// to see in a log.
	ErrUserNotFound = backend.ErrUserNotFound
)
View Source
var ErrNilSessionManager = errors.New("auth: nil session manager")

ErrNilSessionManager is returned by ActiveSessions when called on a nil or zero-initialised SessionManager (one not created via NewSessionManager).

View Source
var ErrSessionStoreNotIterable = errors.New("auth: session store does not support enumeration")

ErrSessionStoreNotIterable is returned by ActiveSessions when the configured session store does not support enumeration (a custom store implementing neither All nor AllCtx; the built-in stores all support it since the non-functional cookie store was removed in v0.12.0, DEP-2026-006).

View Source
var ErrTokenRevoked = errors.New("auth: token has been revoked")

ErrTokenRevoked reports a token that validated correctly and has been revoked. It is deliberately distinct from a signature or expiry failure: a caller that logs "invalid token" for a revoked one cannot tell an attack from a sign-out.

Functions

func CheckPassword

func CheckPassword(password, hash string) bool

CheckPassword compares a plaintext password against a bcrypt hash. Returns true if they match, false otherwise (including on malformed hashes).

func ClientIPFromRequest

func ClientIPFromRequest(r *http.Request) string

ClientIPFromRequest returns the client IP of a request: the host part of r.RemoteAddr.

It reads no forwarding header on purpose. X-Forwarded-For and X-Real-IP are set by whoever sent the request unless a proxy the deployment trusts overwrote them, and the router's RealIP middleware already rewrites RemoteAddr from those headers — for the proxies listed in trusted_proxies, and for nobody else. Reading the headers again here would let any client choose the IP that lands in its session metadata and in the audit trail, which is exactly what trusted_proxies exists to prevent.

func ContextWithClaims

func ContextWithClaims(ctx context.Context, claims *Claims) context.Context

ContextWithClaims returns a copy of ctx carrying the given claims — exactly what JWTManager.Middleware stores after validating a bearer token, including the observability user-id propagation for log attribution. It is the bridge for applications that authenticate by other means — typically a server-side session — and still need the authorization layer (Enforcer.Middleware, Enforcer.RequireRole) to see the request's subject: load the session, build a *Claims carrying the subject and role, and wrap the request context in a middleware that runs before those checks. A nil claims returns ctx unchanged.

Two caveats. Claims values are trusted as-is — build them only from server-side state (a session), never from request-supplied input. And an empty claims.UserID still overwrites the observability user-id slot with ""; pass a non-empty UserID when injecting a real subject.

func FederatedCallbackPath

func FederatedCallbackPath(instance string) string

func FederatedStartPath

func FederatedStartPath(instance string) string

FederatedStartPath and FederatedCallbackPath are the routes an application MOUNTS for an instance. The framework does not register them: it owns the flow (Begin issues the anti-forgery state and holds the pending sign-in, Complete verifies it before the provider is consulted), and the application owns the two handlers, because what happens after a successful callback — which session manager, which landing page, which account gets linked — is the application's decision and not the framework's.

Use these helpers rather than writing the paths by hand: CallbackURL is derived from the same functions, so the URL logged at startup and registered with the identity provider is the one your route actually serves. Hand-written paths drift, and a callback that does not match is a sign-in that fails only in production.

mux.Handle(auth.FederatedStartPath("corp"), startHandler(set, "corp"))
mux.Handle(auth.FederatedCallbackPath("corp"), callbackHandler(set, "corp"))

They are functions rather than constants because the instance name is in them.

func HashPassword

func HashPassword(password string) (string, error)

HashPassword generates a bcrypt hash of the given password with cost 12.

func RegisterBackend

func RegisterBackend(name string, factory BackendFactory) error

RegisterBackend makes an authentication backend selectable by name from configuration. It delegates to backend.Register; a new backend should call that one and avoid importing this package at all.

func RegisterSessionStore

func RegisterSessionStore(name string, factory SessionStoreFactory) error

RegisterSessionStore makes a session backend selectable by name from configuration (`session_store`). It delegates to sessionstore.Register.

func RegisteredBackends

func RegisteredBackends() []string

RegisteredBackends returns every selectable backend name, sorted.

func RegisteredSessionStores

func RegisteredSessionStores() []string

RegisteredSessionStores returns every selectable store name, sorted.

func RuntimeMetadataMiddleware

func RuntimeMetadataMiddleware(sm *SessionManager, identity SessionRuntimeIdentity, minInterval time.Duration) func(http.Handler) http.Handler

RuntimeMetadataMiddleware updates runtime metadata fields in existing sessions so shared stores can expose where each session is actively served.

Types

type Backend

type Backend = backend.Backend

Backend authenticates a username and password against one identity source. See backend.Backend.

func NewUserProviderBackend

func NewUserProviderBackend(name string, provider UserProvider) (Backend, error)

NewUserProviderBackend wraps a UserProvider as an authentication backend, so an application's own user table takes its place in the chain alongside a directory or any other source.

It is usually the LAST entry: `[ldap, local]` means the directory answers first and the local table is what still works the morning the directory does not.

TIMING. This adapter cannot make an implementation constant-time; it only refrains from making things worse. Every failure — user absent, password wrong, lookup error — leaves through one path with one error, so the adapter adds no distinguishable branch. Equalising the WORK is the provider's job: if ValidateCredentials returns early for an unknown user without hashing anything, it answers faster than for a real user with a wrong password, and that difference is a user enumerator no wrapper can hide. Hash against a dummy value before returning, the way the admin login in this suite already does.

type BackendConfig

type BackendConfig = backend.Config

BackendConfig carries a backend's own `auth.<name>.*` subtree.

type BackendFactory

type BackendFactory = backend.Factory

BackendFactory builds a Backend from its configuration subtree.

type Chain

type Chain struct {
	// contains filtered or unexported fields
}

Chain authenticates against an ORDERED list of backends, stopping at the first one that accepts.

The order is the point, and it is why this is a list and not the map the backends themselves live in. The deployment everyone actually wants is "the directory first, a local account second": when the directory is unreachable, somebody still has to be able to get in and fix it. A map cannot express that, and a set of independent backends cannot either.

func NewChain

func NewChain(names ...string) (*Chain, error)

NewChain builds a chain from registered backend names, in order.

It is the convenience form for a chain whose backends need no configuration of their own; NewChainFrom is the full form.

func NewChainFrom

func NewChainFrom(cfg ChainConfig) (*Chain, error)

NewChainFrom builds a chain from registered backend names, in order, handing each backend the configuration subtree that belongs to it.

func (*Chain) Authenticate

func (c *Chain) Authenticate(ctx context.Context, username, password string) (*User, error)

Authenticate walks the chain in order and returns the first acceptance.

A backend that REJECTS ends the attempt; one that is UNAVAILABLE is skipped. The two look alike — neither produced a user — but their first causes are opposite, and giving them the same effect is a fail-open: a rejection is the identity source's verdict on these credentials, while an unreachable backend proves nothing at all.

Concretely, that is what stops a stale local row from being a bypass. If an employee's directory account is revoked and her local account still carries the old password, the directory's rejection ends the attempt and the local backend never gets its turn. Only an unreachable directory falls through to it, which is the break-glass path the ordering exists for.

The consequence is worth stating plainly: a chain is a FALLBACK for unavailability, not a way to federate several user populations. Every account must be acceptable to the first backend that recognises the request, because anything behind a rejection is unreachable by design.

Rejection covers both "no such user" and "wrong password": pkg/auth/backend collapses them into ErrInvalidCredentials on purpose, since a backend that told them apart would publish a user enumerator — and, because the chain stops on rejection, would publish it for every backend behind it too.

The caller can still tell the two outcomes apart. If every backend rejected, the answer is ErrInvalidCredentials. If any backend was unavailable and none accepted, the error says so and names it, because "wrong password" and "the directory is down" send an operator hunting in different places.

func (*Chain) Names

func (c *Chain) Names() []string

Names returns the chain's backends in order.

type ChainConfig

type ChainConfig struct {
	// Backends is the ORDERED list of registered names to consult.
	Backends []string

	// ProviderConfig maps a backend name to its `auth.<name>.*` subtree.
	// A name with no entry gets an empty BackendConfig, which is the
	// normal case for a backend that needs no settings.
	ProviderConfig map[string]map[string]any
}

ChainConfig declares the ordered chain and carries each backend's own configuration subtree.

type Claims

type Claims struct {
	UserID   string `json:"uid"`
	Username string `json:"username"`
	Role     string `json:"role"`
	// Roles is every role the identity holds. It exists because an
	// identity provider answers with a LIST — three group memberships, say
	// — and a single Role has one slot for them. Role stays the primary
	// one, and stays what every existing reader looks at, so nothing that
	// ignores this field changes behaviour.
	Roles []string `json:"roles,omitempty"`
	jwt.RegisteredClaims
}

Claims holds the JWT payload with user identity information.

func ClaimsFromContext

func ClaimsFromContext(ctx context.Context) (*Claims, bool)

ClaimsFromContext extracts JWT claims from the request context. Returns nil, false if no claims are present.

func (*Claims) AllRoles added in v1.28.0

func (c *Claims) AllRoles() []string

AllRoles returns the primary role followed by the rest, de-duplicated and without blanks — the list a policy layer iterates.

func (*Claims) HasRole added in v1.28.0

func (c *Claims) HasRole(role string) bool

HasRole reports whether the identity holds a role, primary or otherwise. The comparison is case-insensitive, which is what an identity provider's group names make necessary.

type FederatedConfig

type FederatedConfig struct {
	// Instances are the declarations, in the order an operator wrote them
	// — which is the order sign-in buttons should appear in.
	Instances []FederatedInstance

	// ProviderConfig maps an INSTANCE name to its `auth.<name>.*` subtree,
	// the same channel a credential backend is configured through.
	ProviderConfig map[string]map[string]any

	// CallbackBase is the absolute base URL this application is reached
	// at, without a trailing slash — "https://app.example.com". The
	// callback URL a provider registers with its identity provider is
	// built from it, so it has to be the address the BROWSER uses, not
	// the one the process binds.
	CallbackBase string

	// PendingTTL bounds how long a started sign-in may sit unfinished.
	// Zero means DefaultFederatedPendingTTL.
	PendingTTL time.Duration
}

FederatedConfig builds the set of configured identity providers.

type FederatedInstance

type FederatedInstance struct {
	// Name identifies this instance: the `auth.<name>.*` subtree, the URL
	// segment, the value a sign-in link asks for.
	Name string `koanf:"name"`

	// Provider is the registered type — "oidc", "saml".
	Provider string `koanf:"provider"`

	// DisplayName is what a sign-in button says. Empty falls back to Name,
	// which is right for "corp" and wrong for nothing.
	DisplayName string `koanf:"display_name"`
}

FederatedInstance is one identity provider an operator declared.

Name and Provider are different things on purpose. Name is the instance — what appears in the URL and what the operator writes their settings under — and Provider is the registered type that implements it. Two declarations with provider: oidc and different names are two identity providers, which is the ordinary case (a corporate tenant and a partner one) and the reason the registry is keyed by type.

type FederatedSet

type FederatedSet struct {
	// contains filtered or unexported fields
}

FederatedSet is the configured identity providers and the custody of the flows in progress.

The custody is the point. A provider never sees the anti-forgery state: this type issues it, holds it, and refuses a callback that does not carry it back — before the provider is asked anything. A redirect flow without that check works perfectly well until somebody attacks it, which is exactly the kind of omission that does not announce itself, so the framework does not offer the choice.

func NewFederatedSet

func NewFederatedSet(cfg FederatedConfig) (*FederatedSet, error)

NewFederatedSet builds the configured providers, failing on the first declaration that cannot be honoured.

func (*FederatedSet) Begin

func (s *FederatedSet) Begin(ctx context.Context, instance string) (redirectURL, stateToken string, err error)

Begin starts a sign-in and returns where to send the browser, plus the opaque state token the caller must give back on the callback.

The token is what a caller stores in a short-lived cookie. It is not the provider's state and the provider never sees it.

func (*FederatedSet) CallbackURL

func (s *FederatedSet) CallbackURL(instance string) string

CallbackURL is the absolute URL an identity provider must send the browser back to for one instance. An operator registers this value with their identity provider, and `nucleus doctor` prints it, so it is derived in exactly one place.

func (*FederatedSet) Complete

func (s *FederatedSet) Complete(ctx context.Context, instance, stateToken string, r *http.Request) (*federated.User, error)

Complete verifies the callback and returns the authenticated identity.

stateToken is what Begin returned. A callback that arrives without it, with one this set did not issue, or with one already spent, is refused HERE — the provider is not called, so a provider cannot forget to check.

func (*FederatedSet) Instances

func (s *FederatedSet) Instances() []FederatedInstance

Instances returns the declarations, in order, for a sign-in page that needs to render a button per identity provider.

func (*FederatedSet) Names

func (s *FederatedSet) Names() []string

Names returns the configured instance names, in declaration order.

func (*FederatedSet) PendingCount

func (s *FederatedSet) PendingCount() int

PendingCount reports how many sign-ins are in flight. It exists for tests and for a metric; it is not part of the flow.

type JWK

type JWK struct {
	Kid string `json:"kid"`
	Kty string `json:"kty"`
	Alg string `json:"alg"`
	Use string `json:"use"`
	N   string `json:"n,omitempty"`
	E   string `json:"e,omitempty"`
	Crv string `json:"crv,omitempty"`
	X   string `json:"x,omitempty"`
	Y   string `json:"y,omitempty"`
}

JWK is the wire shape of an RFC 7517 JSON Web Key. The Use field is always "sig" for keys produced by this package; HMAC keys are not emitted, so the kty field is "RSA" (RS256) or "EC" (ES256).

RSA keys populate N and E; EC keys populate Crv, X and Y. The omitempty tags keep each emitted key minimal — an EC key carries no n/e and an RSA key carries no crv/x/y.

type JWKSet

type JWKSet struct {
	Keys []JWK `json:"keys"`
}

JWKSet is the wire shape of an RFC 7517 JSON Web Key Set.

type JWTManager

type JWTManager struct {
	// contains filtered or unexported fields
}

JWTManager handles JWT token generation and validation. It supports two modes that coexist:

  • Legacy single-secret HS256: built via NewJWTManager. Tokens have no "kid" header; Validate falls back to the single secret when the incoming token lacks a kid.
  • Multi-key with rotation: built via NewJWTManagerFromKeys or by calling RotateKey on an existing manager. Tokens carry a "kid" header; Validate looks up the key by kid. Adding a new key keeps existing tokens valid until they expire — the operator path for zero-downtime rotation.

JWKSHandler exposes the asymmetric portion of the keyset over an RFC 7517 / RFC 7518 JSON shape; HMAC keys are intentionally omitted so the published endpoint cannot leak shared secrets.

func NewJWTManager

func NewJWTManager(secret string, expiry time.Duration, issuer ...string) *JWTManager

NewJWTManager creates a single-secret HS256 manager. Backwards compatible: tokens carry no "kid" header and Validate uses the single secret. Use NewJWTManagerFromKeys (or RotateKey on the returned manager) to opt into rotation.

NewJWTManager panics when secret is shorter than 32 bytes (256 bits, the HMAC-SHA256 output width): a short secret yields forgeable tokens, and a weak-key deployment must crash at startup rather than serve — the same regexp.MustCompile-style posture as CSRFMiddleware (ADR-006). The signature cannot grow an error return (frozen public surface); callers who want an error path instead of a panic should configure the manager through pkg/app (`jwt_secret` / `jwt_keys`), which validates with a returned error before ever reaching this constructor.

func NewJWTManagerFromKeys

func NewJWTManagerFromKeys(keys []SigningKey, currentKID string, expiry time.Duration, issuer ...string) (*JWTManager, error)

NewJWTManagerFromKeys creates a manager that signs with the key identified by currentKID and accepts tokens signed by any key in keys. At least one key is required; currentKID must match one of them.

func (*JWTManager) Audience

func (m *JWTManager) Audience() string

Audience returns the audience Generate stamps and Validate requires, or "" when none is configured.

func (*JWTManager) CurrentKID

func (m *JWTManager) CurrentKID() string

CurrentKID returns the kid that Generate will stamp into new tokens, or empty when the manager is in legacy single-secret mode.

func (*JWTManager) Generate

func (m *JWTManager) Generate(userID, username, role string) (string, error)

Generate creates a signed JWT token for the given user claims. In multi-key mode the current key's algorithm is used and its kid is stamped into the token header. In legacy mode HS256 is used and the header carries no kid.

func (*JWTManager) GenerateWithRoles added in v1.28.0

func (m *JWTManager) GenerateWithRoles(userID, username, role string, roles []string) (string, error)

GenerateWithRoles mints a token carrying every role the identity holds. role stays the primary one — it is what the existing claim, the policy subject resolver and the rate limiter read — and roles carries the full set an identity provider returned. Nothing that reads only role changes behaviour.

func (*JWTManager) JWKS

func (m *JWTManager) JWKS() JWKSet

JWKS returns the public key set in the canonical JWK Set shape. Use JWKSHandler for a ready-to-mount HTTP handler; JWKS itself is exposed for callers that need the raw structure (e.g. embedding in OIDC discovery responses).

func (*JWTManager) JWKSHandler

func (m *JWTManager) JWKSHandler() http.HandlerFunc

JWKSHandler returns an HTTP handler that serves the manager's public key set in JWK Set format (RFC 7517 / RFC 7518). Only asymmetric keys are published — HMAC keys are intentionally excluded so the endpoint cannot leak shared secrets.

Mount the handler at the canonical path (`/.well-known/jwks.json`) or wherever your deployment expects relying parties to discover it.

func (*JWTManager) Middleware

func (m *JWTManager) Middleware() func(http.Handler) http.Handler

Middleware returns an HTTP middleware that extracts and validates the JWT token from the Authorization header (Bearer scheme). On success, the claims are stored in the request context and can be retrieved via ClaimsFromContext.

func (*JWTManager) OptionalJWTMiddleware

func (m *JWTManager) OptionalJWTMiddleware() func(http.Handler) http.Handler

OptionalJWTMiddleware is like Middleware but does not reject requests without a token. If a valid token is present, claims are added to the context. If no token or an invalid token is present, the request proceeds without claims.

func (*JWTManager) RemoveKey

func (m *JWTManager) RemoveKey(kid string) error

RemoveKey drops a key from the verification set. Tokens signed with that kid will be rejected on the next Validate call. The current signing key cannot be removed; promote a different key with RotateKey first.

func (*JWTManager) Revoke added in v1.28.0

func (m *JWTManager) Revoke(ctx context.Context, tokenString string) error

Revoke refuses a token from now until it would have expired. It is the operation behind "sign out", "this device is lost" and "the key leaked", and it needs a revocation store: without one there is nowhere to record the decision, and the call says so rather than pretending.

func (*JWTManager) RotateKey

func (m *JWTManager) RotateKey(key SigningKey, makeCurrent bool) error

RotateKey adds a key to the verification set. When makeCurrent is true, future Generate calls use this key as the signing key. Existing tokens (signed with the previous current key) remain valid as long as that key stays in the set. Operators are responsible for removing keys with RemoveKey after the access-token lifetime expires.

func (*JWTManager) SetAudience

func (m *JWTManager) SetAudience(aud string)

SetAudience makes Generate stamp aud into new tokens and Validate reject any token that does not carry it. Empty (the default) leaves aud unchecked. pkg/app sets it from `jwt_audience`.

func (*JWTManager) SetRevocationStore added in v1.28.0

func (m *JWTManager) SetRevocationStore(store RevocationStore)

SetRevocationStore makes Validate refuse a token whose id has been revoked, and Revoke record one. Nil (the default) keeps the cheap property a bearer token exists for: no lookup per request. pkg/app wires it from the session store when `jwt_revocation` is on.

func (*JWTManager) Validate

func (m *JWTManager) Validate(tokenString string) (*Claims, error)

Validate parses and validates a JWT token string. Returns the claims if valid.

Resolution order:

  1. If the token header carries a "kid" matching a key in the verification set, that key is used.
  2. Otherwise, if the manager has a legacy single secret, it is used (HS256 only).
  3. Otherwise, the token is rejected — a multi-key manager will not accept a token without a kid.

func (*JWTManager) ValidateContext added in v1.28.0

func (m *JWTManager) ValidateContext(ctx context.Context, tokenString string) (*Claims, error)

ValidateContext is Validate with a context, which the revocation store needs — the one lookup a validation can make that leaves the process.

type MemcachedSessionStore

type MemcachedSessionStore struct {
	// contains filtered or unexported fields
}

MemcachedSessionStore persists sessions in Memcached with key TTL.

func NewMemcachedSessionStore

func NewMemcachedSessionStore(client *memcache.Client, keyPrefix string) (*MemcachedSessionStore, error)

NewMemcachedSessionStore creates a Memcached-backed session store from an existing client.

func NewMemcachedSessionStoreFromServers

func NewMemcachedSessionStoreFromServers(servers []string, keyPrefix string) (*MemcachedSessionStore, *memcache.Client, error)

NewMemcachedSessionStoreFromServers creates a memcache.Client and a Memcached session store.

func (*MemcachedSessionStore) All

func (s *MemcachedSessionStore) All() (map[string][]byte, error)

All returns all active sessions visible from the configured key prefix. Note: Memcached doesn't support key listing, so this returns an empty map. Use Redis or SQL if you need to list all sessions.

func (*MemcachedSessionStore) AllCtx

func (s *MemcachedSessionStore) AllCtx(ctx context.Context) (map[string][]byte, error)

AllCtx returns all active sessions. Note: Memcached doesn't support key listing, so this returns an empty map. Use Redis or SQL if you need to list all sessions.

func (*MemcachedSessionStore) Commit

func (s *MemcachedSessionStore) Commit(token string, b []byte, expiry time.Time) error

Commit stores the session payload for token with absolute expiry.

func (*MemcachedSessionStore) CommitCtx

func (s *MemcachedSessionStore) CommitCtx(ctx context.Context, token string, b []byte, expiry time.Time) error

CommitCtx stores the session payload in Memcached with a TTL derived from expiry.

func (*MemcachedSessionStore) Delete

func (s *MemcachedSessionStore) Delete(token string) error

Delete removes the session token from the store.

func (*MemcachedSessionStore) DeleteCtx

func (s *MemcachedSessionStore) DeleteCtx(ctx context.Context, token string) error

DeleteCtx removes the session token from Memcached.

func (*MemcachedSessionStore) Find

func (s *MemcachedSessionStore) Find(token string) ([]byte, bool, error)

Find retrieves the session payload for token.

func (*MemcachedSessionStore) FindCtx

func (s *MemcachedSessionStore) FindCtx(ctx context.Context, token string) ([]byte, bool, error)

FindCtx retrieves the session payload from Memcached.

type MemoryRevocationStore added in v1.28.0

type MemoryRevocationStore struct {
	// contains filtered or unexported fields
}

MemoryRevocationStore keeps revocations in this process. It is the right store for a single-process deployment and the wrong one for several: a token revoked on one node stays valid on the others. Use NewSessionStoreRevocations with a shared session store for that.

func NewMemoryRevocationStore added in v1.28.0

func NewMemoryRevocationStore() *MemoryRevocationStore

NewMemoryRevocationStore returns an in-process revocation store.

func (*MemoryRevocationStore) Len added in v1.28.0

func (s *MemoryRevocationStore) Len() int

Len reports how many revocations are held, live ones only. It exists for tests and for an operator endpoint that wants to show the size of the list rather than guess at it.

func (*MemoryRevocationStore) Revoke added in v1.28.0

func (s *MemoryRevocationStore) Revoke(_ context.Context, id string, expiresAt time.Time) error

Revoke implements RevocationStore.

func (*MemoryRevocationStore) Revoked added in v1.28.0

func (s *MemoryRevocationStore) Revoked(_ context.Context, id string) (bool, error)

Revoked implements RevocationStore.

type RedisSessionStore

type RedisSessionStore struct {
	// contains filtered or unexported fields
}

RedisSessionStore persists sessions in Redis with key TTL.

func NewRedisSessionStore

func NewRedisSessionStore(client redis.UniversalClient, keyPrefix string) (*RedisSessionStore, error)

NewRedisSessionStore creates a Redis-backed session store from an existing client.

func NewRedisSessionStoreFromURL

func NewRedisSessionStoreFromURL(rawURL, keyPrefix string) (*RedisSessionStore, *redis.Client, error)

NewRedisSessionStoreFromURL creates a redis.Client and a Redis session store.

func (*RedisSessionStore) All

func (s *RedisSessionStore) All() (map[string][]byte, error)

All returns all active sessions visible from the configured key prefix.

func (*RedisSessionStore) AllCtx

func (s *RedisSessionStore) AllCtx(ctx context.Context) (map[string][]byte, error)

AllCtx returns all active sessions visible from the configured key prefix.

func (*RedisSessionStore) Commit

func (s *RedisSessionStore) Commit(token string, b []byte, expiry time.Time) error

Commit stores the session payload for token with absolute expiry.

func (*RedisSessionStore) CommitCtx

func (s *RedisSessionStore) CommitCtx(ctx context.Context, token string, b []byte, expiry time.Time) error

CommitCtx stores the session payload in Redis with a TTL derived from expiry.

func (*RedisSessionStore) Delete

func (s *RedisSessionStore) Delete(token string) error

Delete removes the session token from the store.

func (*RedisSessionStore) DeleteCtx

func (s *RedisSessionStore) DeleteCtx(ctx context.Context, token string) error

DeleteCtx removes the session token from Redis.

func (*RedisSessionStore) Find

func (s *RedisSessionStore) Find(token string) ([]byte, bool, error)

Find retrieves the session payload for token.

func (*RedisSessionStore) FindCtx

func (s *RedisSessionStore) FindCtx(ctx context.Context, token string) ([]byte, bool, error)

FindCtx retrieves the session payload from Redis.

type RevocationStore added in v1.28.0

type RevocationStore interface {
	// Revoke records an identifier as revoked until expiresAt.
	Revoke(ctx context.Context, id string, expiresAt time.Time) error
	// Revoked reports whether an identifier has been revoked.
	Revoked(ctx context.Context, id string) (bool, error)
}

RevocationStore records the identifiers of tokens that must no longer be accepted, until they expire on their own.

A bearer token is valid until it expires, and that is the property that makes it cheap: no lookup, no shared state. Revocation buys back the one case the property cannot cover — a token that leaked, or a sign-out that has to mean something before the expiry — and it buys it at the price of a lookup per request. Both halves of that trade are deliberate, so the store is opt-in: a deployment that does not set one keeps the cheap property, and Validate does not touch it.

An entry is written with the token's OWN expiry, so the store never grows beyond the tokens that are still live.

func NewSessionStoreRevocations added in v1.28.0

func NewSessionStoreRevocations(store SessionStore, prefix string) (RevocationStore, error)

NewSessionStoreRevocations adapts a session store into a RevocationStore. The prefix keeps revocation keys from colliding with session tokens in a shared keyspace; it defaults to "revoked:".

type SQLSessionStore

type SQLSessionStore struct {
	// contains filtered or unexported fields
}

SQLSessionStore persists sessions in a SQL table.

func NewSQLSessionStore

func NewSQLSessionStore(db *sql.DB, cfg SQLSessionStoreConfig) (*SQLSessionStore, error)

NewSQLSessionStore builds a SQL-backed session store and ensures table schema.

func (*SQLSessionStore) All

func (s *SQLSessionStore) All() (map[string][]byte, error)

All returns all non-expired sessions.

func (*SQLSessionStore) AllCtx

func (s *SQLSessionStore) AllCtx(ctx context.Context) (map[string][]byte, error)

AllCtx returns all non-expired sessions.

func (*SQLSessionStore) Commit

func (s *SQLSessionStore) Commit(token string, b []byte, expiry time.Time) error

Commit stores the session payload for token with absolute expiry.

func (*SQLSessionStore) CommitCtx

func (s *SQLSessionStore) CommitCtx(ctx context.Context, token string, b []byte, expiry time.Time) error

CommitCtx stores the session payload for token with absolute expiry.

func (*SQLSessionStore) Delete

func (s *SQLSessionStore) Delete(token string) error

Delete removes the session token from the store.

func (*SQLSessionStore) DeleteCtx

func (s *SQLSessionStore) DeleteCtx(ctx context.Context, token string) error

DeleteCtx removes the session token from the store.

func (*SQLSessionStore) Find

func (s *SQLSessionStore) Find(token string) ([]byte, bool, error)

Find retrieves the session payload for token.

func (*SQLSessionStore) FindCtx

func (s *SQLSessionStore) FindCtx(ctx context.Context, token string) ([]byte, bool, error)

FindCtx retrieves the session payload for token.

type SQLSessionStoreConfig

type SQLSessionStoreConfig struct {
	DatabaseURL string
	TableName   string
}

SQLSessionStoreConfig configures a SQL-backed SCS store.

type SessionCache

type SessionCache struct {
	// contains filtered or unexported fields
}

SessionCache provides a cache mechanism scoped to an individual user session. Unlike the global application cache, session cache data is automatically isolated per session and cleaned up when the session expires or is destroyed. This is perfect for storing temporary, user-specific data like form data, temporary calculations, API responses, or any other ephemeral data.

func NewSessionCache

func NewSessionCache(sm *SessionManager) *SessionCache

NewSessionCache creates a new session cache instance.

func (*SessionCache) Flush

func (c *SessionCache) Flush(ctx context.Context)

Flush removes all values from the session cache. Only cache-prefixed entries are touched; every other session key is left alone.

func (*SessionCache) Forget

func (c *SessionCache) Forget(ctx context.Context, key string)

Forget removes a value from the session cache.

func (*SessionCache) Get

func (c *SessionCache) Get(ctx context.Context, key string) (string, bool)

Get retrieves a value from the session cache. Returns the value and a boolean indicating if the key exists and is not expired.

func (*SessionCache) GetBool

func (c *SessionCache) GetBool(ctx context.Context, key string) (bool, bool)

GetBool retrieves a bool value from the session cache.

func (*SessionCache) GetInt

func (c *SessionCache) GetInt(ctx context.Context, key string) (int, bool)

GetInt retrieves an int value from the session cache.

func (*SessionCache) Has

func (c *SessionCache) Has(ctx context.Context, key string) bool

Has checks if a key exists in the session cache and is not expired.

func (*SessionCache) Put

func (c *SessionCache) Put(ctx context.Context, key string, value string, ttl time.Duration)

Put stores a value in the session cache with an optional TTL. If ttl is 0, the value will persist until the session expires.

func (*SessionCache) PutBool

func (c *SessionCache) PutBool(ctx context.Context, key string, value bool, ttl time.Duration)

PutBool stores a bool value in the session cache with an optional TTL.

func (*SessionCache) PutInt

func (c *SessionCache) PutInt(ctx context.Context, key string, value int, ttl time.Duration)

PutInt stores an int value in the session cache with an optional TTL.

func (*SessionCache) Remember

func (c *SessionCache) Remember(ctx context.Context, key string, ttl time.Duration, fn func() (string, error)) (string, error)

Remember retrieves a value from the session cache. If the key does not exist, it executes the provided function and stores the result with the given TTL.

func (*SessionCache) RememberBool

func (c *SessionCache) RememberBool(ctx context.Context, key string, ttl time.Duration, fn func() (bool, error)) (bool, error)

RememberBool retrieves a bool value from the session cache. If the key does not exist, it executes the provided function and stores the result with the given TTL.

func (*SessionCache) RememberInt

func (c *SessionCache) RememberInt(ctx context.Context, key string, ttl time.Duration, fn func() (int, error)) (int, error)

RememberInt retrieves an int value from the session cache. If the key does not exist, it executes the provided function and stores the result with the given TTL.

type SessionConfig

type SessionConfig struct {
	Lifetime    time.Duration // Session lifetime (default: 72h)
	IdleTimeout time.Duration // Optional inactivity timeout (default: disabled)
	Secure      bool          // Cookie Secure flag (set true in production)
	Path        string        // Cookie path (default: "/")
	Domain      string        // Cookie domain (default: host-only)
	CookieName  string        // Cookie name (default: "session")
	SameSite    string        // Cookie SameSite: lax|strict|none (default: lax)
}

SessionConfig configures the session manager.

type SessionInfo

type SessionInfo struct {
	// Token is the raw session token (the store key) — a bearer credential.
	Token string
	// Deadline is the absolute expiry the codec recorded for the session.
	Deadline time.Time
	// Values are the decoded session key/value pairs (may be sensitive).
	Values map[string]any
}

SessionInfo is a decoded snapshot of one stored session, returned by SessionManager.ActiveSessions. It is distinct from the lifecycle-event payload observability/hooks.SessionInfo — this is an enumeration/admin snapshot.

SECURITY: SessionInfo carries live secrets. Token is the raw session token, a bearer credential — anyone who obtains it can impersonate the session until it expires. Values is the full decoded session map and MAY hold user identifiers, CSRF tokens, and flash data. ActiveSessions is intended ONLY for a trusted, in-process operator/admin surface (orbit). NEVER serialize Token or Values to an untrusted response, an access log, or any sink that leaves the process without deliberate redaction. The raw Token is retained (not hashed) on purpose: an operator tool acts on a session — e.g. revokes it through the store — which needs the exact token.

type SessionManager

type SessionManager struct {
	// contains filtered or unexported fields
}

SessionManager wraps alexedwards/scs for server-side session management. Sessions can be backed by in-memory storage (default), SQL, or Redis.

func NewSessionManager

func NewSessionManager(cfg SessionConfig) *SessionManager

NewSessionManager creates a session manager with the given configuration. By default it uses in-memory storage. Call SetStore to use Redis, SQL, or another backend.

func (*SessionManager) ActiveSessions

func (s *SessionManager) ActiveSessions(ctx context.Context) ([]SessionInfo, error)

ActiveSessions returns a decoded snapshot of every session currently held by the store, for a session-management/observability surface (e.g. an admin "active sessions" view). It requires a store that supports enumeration — the memory, SQL, Redis and Memcached stores all do; a custom store implementing neither All nor AllCtx yields ErrSessionStoreNotIterable.

A payload that fails to decode is skipped rather than failing the whole call, so one corrupt entry cannot blind the operator to every other session. The returned slice is a point-in-time snapshot, unordered; callers sort/filter as needed.

Cost: O(N) in the number of stored sessions — it loads and decodes the whole store into memory in one pass (scs stores expose no server-side pagination), so cap what you render at the call site for a very large store.

This packages a capability already reachable through the SCS() escape hatch (Store + Codec) into one typed call; it exposes no scs type on the API. The returned Values/Token are sensitive — see SessionInfo (SECURITY).

func (*SessionManager) Destroy

func (s *SessionManager) Destroy(ctx context.Context) error

Destroy deletes the entire session.

func (*SessionManager) Exists

func (s *SessionManager) Exists(ctx context.Context, key string) bool

Exists checks if a key exists in the session.

func (*SessionManager) Flash

func (s *SessionManager) Flash(ctx context.Context, key, value string)

Flash stores a key-value pair in the session that will be available in the next request only. After the next request, the data is automatically deleted. This is useful for status messages (e.g., "Task completed successfully").

func (*SessionManager) FlashBool

func (s *SessionManager) FlashBool(ctx context.Context, key string, value bool)

FlashBool stores a bool value in flash data.

func (*SessionManager) FlashInt

func (s *SessionManager) FlashInt(ctx context.Context, key string, value int)

FlashInt stores an int value in flash data.

func (*SessionManager) Forget

func (s *SessionManager) Forget(ctx context.Context, keys []string)

Forget removes multiple keys from the session in one operation.

func (*SessionManager) GetBool

func (s *SessionManager) GetBool(ctx context.Context, key string) bool

GetBool retrieves a bool value from the session.

func (*SessionManager) GetFlash

func (s *SessionManager) GetFlash(ctx context.Context, key string) string

GetFlash retrieves a flash value for the current request.

func (*SessionManager) GetFlashBool

func (s *SessionManager) GetFlashBool(ctx context.Context, key string) bool

GetFlashBool retrieves a flash bool value for the current request.

func (*SessionManager) GetFlashInt

func (s *SessionManager) GetFlashInt(ctx context.Context, key string) int

GetFlashInt retrieves a flash int value for the current request.

func (*SessionManager) GetInt

func (s *SessionManager) GetInt(ctx context.Context, key string) int

GetInt retrieves an int value from the session.

func (*SessionManager) GetString

func (s *SessionManager) GetString(ctx context.Context, key string) string

GetString retrieves a string value from the session.

func (*SessionManager) HasSession added in v1.28.0

func (s *SessionManager) HasSession(ctx context.Context) (present bool)

HasSession reports whether this context carries session data — that is, whether the request went through Middleware.

It exists because the session library panics on a context that never passed through its middleware, and "is there a session here?" is a question application code legitimately asks: a handler mounted outside the session middleware, a background job reusing a helper, a service called from a test. Turning that into a panic makes a mounting mistake look like a crash in unrelated code — which is exactly how it was found.

The recover is deliberate and confined to this one probe. Every other accessor still panics, because using one on a context with no session is a programming error; this is the call that ASKS.

func (*SessionManager) Invalidate

func (s *SessionManager) Invalidate(ctx context.Context) error

Invalidate regenerates the session ID and removes all data from the session. This is useful for logout or when you want to completely reset the session.

func (*SessionManager) Keep

func (s *SessionManager) Keep(ctx context.Context, keys []string)

Keep keeps specific flash data keys for one more request.

func (*SessionManager) Middleware

func (s *SessionManager) Middleware() func(http.Handler) http.Handler

Middleware returns the session middleware that must be applied to the router for session handling to work.

func (*SessionManager) Now

func (s *SessionManager) Now(ctx context.Context, key, value string)

Now stores a key-value pair that is only available in the current request: the middleware sweeps it when the request ends.

func (*SessionManager) Pull

func (s *SessionManager) Pull(ctx context.Context, key string) string

Pull retrieves a value from the session and deletes it in one operation.

func (*SessionManager) PullBool

func (s *SessionManager) PullBool(ctx context.Context, key string) bool

PullBool retrieves a bool value from the session and deletes it in one operation.

func (*SessionManager) PullInt

func (s *SessionManager) PullInt(ctx context.Context, key string) int

PullInt retrieves an int value from the session and deletes it in one operation.

func (*SessionManager) Put

func (s *SessionManager) Put(ctx context.Context, key, value string)

Put stores a string value in the session.

func (*SessionManager) PutBool

func (s *SessionManager) PutBool(ctx context.Context, key string, value bool)

PutBool stores a bool value in the session.

func (*SessionManager) PutInt

func (s *SessionManager) PutInt(ctx context.Context, key string, value int)

PutInt stores an int value in the session.

func (*SessionManager) Reflash

func (s *SessionManager) Reflash(ctx context.Context)

Reflash keeps all flash data readable now for one more request.

func (*SessionManager) Remove

func (s *SessionManager) Remove(ctx context.Context, key string)

Remove deletes a key from the session.

func (*SessionManager) RenewToken

func (s *SessionManager) RenewToken(ctx context.Context) error

RenewToken generates a new session ID while preserving data. Should be called after login to prevent session fixation.

func (*SessionManager) Revoke added in v1.28.0

func (s *SessionManager) Revoke(ctx context.Context, token string) error

Revoke deletes one stored session by its token, ending it everywhere it was in use — the operation behind "sign out my other devices" and behind an operator ending a session that should not be open.

It is separate from Destroy and Invalidate, which act on the session in the REQUEST context and therefore can only end the caller's own. That asymmetry is why the framework could enumerate sessions and not act on them: the store has always known how to delete a token, and nothing exposed it.

Revoking a token that is not in the store is not an error: the outcome a caller asked for — that session is gone — already holds, and reporting a miss would leak whether a token existed to whoever can call this.

func (*SessionManager) RevokeWhere added in v1.28.0

func (s *SessionManager) RevokeWhere(ctx context.Context, match func(SessionInfo) bool) (int, error)

RevokeWhere deletes every stored session the predicate accepts and returns how many it ended. It is the shape "sign out everywhere except here" actually needs: the caller keeps its own token and revokes the rest.

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

A predicate is used rather than a user id because the framework does not own the key an application stores identity under; the caller does.

func (*SessionManager) SCS

func (s *SessionManager) SCS() *scs.SessionManager

SCS returns the underlying scs.SessionManager for advanced configuration (e.g. setting a custom store for Redis).

func (*SessionManager) SessionStore added in v1.28.0

func (s *SessionManager) SessionStore() SessionStore

SessionStore returns the framework-level store installed by SetSessionStore, or nil when the manager runs on its in-memory default.

It exists so a caller can reuse the store the deployment ALREADY shares across nodes — a revocation list is the case that motivated it — instead of being handed a second Redis to configure for the same purpose.

func (*SessionManager) SetSessionStore

func (s *SessionManager) SetSessionStore(store SessionStore)

SetSessionStore installs a framework SessionStore on the manager, adapting it to the underlying library.

func (*SessionManager) SetStore

func (s *SessionManager) SetStore(store scs.Store)

SetStore sets a custom SCS store implementation (Redis, SQL, etc).

func (*SessionManager) Token added in v1.28.0

func (s *SessionManager) Token(ctx context.Context) string

Token returns the token of the session in this context, or empty when there is none yet. A caller needs it to exclude its own session from a bulk revocation.

type SessionRuntimeIdentity

type SessionRuntimeIdentity struct {
	Pod      string
	Host     string
	Instance string
}

SessionRuntimeIdentity describes the runtime node that handled a session request.

func DetectSessionRuntimeIdentity

func DetectSessionRuntimeIdentity() SessionRuntimeIdentity

DetectSessionRuntimeIdentity infers pod and host identifiers from common Kubernetes/runtime environment variables.

type SessionStore

type SessionStore = sessionstore.Store

SessionStore is the interface a session backend implements. See sessionstore.Store.

func BuildSessionStore

func BuildSessionStore(name string, params SessionStoreParams) (SessionStore, func(context.Context) error, error)

BuildSessionStore resolves a configured store name and builds it. It returns the store (nil means "keep the manager's in-memory default"), an optional shutdown hook, and an error naming the registered stores when the name is unknown.

type SessionStoreFactory

type SessionStoreFactory = sessionstore.Factory

SessionStoreFactory builds a session store plus an optional shutdown hook.

type SessionStoreParams

type SessionStoreParams = sessionstore.Params

SessionStoreParams carries what a store needs to build itself.

type SigningAlgorithm

type SigningAlgorithm string

SigningAlgorithm enumerates the JWT signing algorithms this package supports. New algorithms (e.g. ES256) can be added without changing the public API — extend the switch in signingMethod / verifyMaterial.

const (
	HS256 SigningAlgorithm = "HS256"
	RS256 SigningAlgorithm = "RS256"
	// ES256 is ECDSA with the NIST P-256 curve and SHA-256. Only P-256
	// is supported — P-384 (ES384) and P-521 (ES512) are deliberately
	// out of scope until there is a concrete need; see ADR-005.
	ES256 SigningAlgorithm = "ES256"
)

type SigningKey

type SigningKey struct {
	KID          string
	Algorithm    SigningAlgorithm
	HMACSecret   []byte            // HS256
	RSAPrivate   *rsa.PrivateKey   // RS256
	ECDSAPrivate *ecdsa.PrivateKey // ES256 (P-256 only)
}

SigningKey is one entry in a JWTManager's keyset. Exactly one of the material fields must be set, matching the Algorithm.

type User

type User = backend.User

User represents a minimal authenticated user identity.

type UserProvider

type UserProvider interface {
	// FindByID retrieves a user by their unique identifier.
	FindByID(ctx context.Context, id string) (*User, error)
	// FindByUsername retrieves a user by username (used for login).
	FindByUsername(ctx context.Context, username string) (*User, error)
	// FindByEmail retrieves a user by email address.
	FindByEmail(ctx context.Context, email string) (*User, error)
	// ValidateCredentials checks if the username/password combination is valid.
	// Returns the user if valid, an error otherwise.
	ValidateCredentials(ctx context.Context, username, password string) (*User, error)
}

UserProvider is the interface that applications must implement to integrate their user model with Nucleus's authentication system.

Directories

Path Synopsis
Package apikeys is the credential a program uses to call an API: issued once, shown once, revocable, scoped, and recognisable in a log.
Package apikeys is the credential a program uses to call an API: issued once, shown once, revocable, scoped, and recognisable in a log.
Package backend is the contract a third-party authentication backend implements — and nothing else.
Package backend is the contract a third-party authentication backend implements — and nothing else.
backendtest
Package backendtest is a conformance suite for authentication backends.
Package backendtest is a conformance suite for authentication backends.
Package federated is the contract a browser-redirect identity provider implements — OIDC, SAML, anything where the user leaves for an identity provider and comes back — and nothing else.
Package federated is the contract a browser-redirect identity provider implements — OIDC, SAML, anything where the user leaves for an identity provider and comes back — and nothing else.
oidc
Package oidc is an OpenID Connect provider for the federated sign-in seam: authorization code flow with PKCE, discovery, and an id_token verified against the provider's published keys.
Package oidc is an OpenID Connect provider for the federated sign-in seam: authorization code flow with PKCE, discovery, and an id_token verified against the provider's published keys.
Package secrets resolves opaque reference strings into raw secret bytes for the auth layer — JWT signing keys, primarily.
Package secrets resolves opaque reference strings into raw secret bytes for the auth layer — JWT signing keys, primarily.
Package sessionstore is the contract a third-party session store implements — and nothing else.
Package sessionstore is the contract a third-party session store implements — and nothing else.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL