security

package
v0.29.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package security holds the authorization primitives of the framework.

It is not an optional package: Grant, Policy, sessions and password hashing live in the core because security is the product thesis, not a plugin the user may forget to install.

This package is a bridge. It is removed in v1.0.0; import github.com/arandu-io/hesape/auth directly.

The components moved to github.com/arandu-io/hesape, under new names, and this package is now the old names pointing at them. It answers to five hesape packages, and which one a symbol went to depends on the symbol:

hesape/auth        Grant, Policy, Subject, Action, Authorize, SystemGrant, the sign-in throttle
hesape/session     the session store, the flash, the CSRF token, the cookie name
hesape/hashing     HashPassword, VerifyPassword, NeedsRehash
hesape/encryption  Signer
hesape/http        LocalPath and the intended destination

The death date above is what keeps this from being a second way to import one type. Nothing here holds an implementation: where the name and the signature survived the move it is a Go alias, and where the design diverged it is an envelope that translates and nothing more.

The three envelopes, and what diverged:

SessionStore     hesape/session.RecordStore[Subject] returns a Record that
                 wraps the payload, and the intended destination moved to
                 hesape/http
SessionBackend   hesape/session.Handler renamed all four methods, and
                 github.com/arandu-io/kv implements the old names
SignInThrottle   all three methods gained a context.Context

Index

Constants

View Source
const (
	MaxSignInFailures          = auth.MaxSignInFailures
	MaxSignInFailuresPerClient = auth.MaxSignInFailuresPerClient
	SignInWindow               = auth.SignInWindow
)

The sign-in policy. Three constants and not configuration, for the reason stated on hesape/auth: a lockout somebody can widen from the environment is a lockout somebody widens the morning it fires.

View Source
const FlashCookieName = session.FlashCookieName

FlashCookieName carries the messages and the typed input of a rejected request across the one redirect that follows it.

View Source
const FlashLifetime = session.FlashLifetime

FlashLifetime is how long the messages are worth keeping.

View Source
const IntendedCookieName = hhttp.IntendedCookieName

IntendedCookieName carries the address somebody was going to, between the request a guard turned away and the sign-in that follows it.

View Source
const IntendedLifetime = hhttp.IntendedLifetime

IntendedLifetime is how long the address is worth keeping.

Long enough to find a password in a manager and type it, and short enough that it does not survive to an unrelated sign-in.

View Source
const MaxFlashBytes = session.MaxFlashBytes

MaxFlashBytes is the budget for the signed cookie value.

View Source
const MinPasswordLen = hashing.MinPasswordLen

MinPasswordLen is the shortest password the framework accepts. Length beats composition rules: it is the only parameter that reliably raises the cost of an offline attack.

View Source
const PasswordConfirmationWindow = session.PasswordConfirmationWindow

PasswordConfirmationWindow is how long typing the password again counts for.

View Source
const RememberLifetime = session.RememberLifetime

RememberLifetime is how long a session started with Remember(true) lives.

View Source
const SessionCookieName = session.CookieName

SessionCookieName is the cookie the framework reads and writes. It is fixed on purpose: a configurable cookie name buys nothing and breaks the CSRF binding when two parts of a project disagree about it.

Renamed on the way to hesape: it is session.CookieName there, because the package name already says which cookie it is.

Variables

View Source
var (
	// ErrNoSession means the request carries no session cookie, or the cookie
	// signature does not match the application key.
	ErrNoSession = session.ErrNoSession

	// ErrSessionExpired means the session id is well formed but the backend no
	// longer holds it -- expired, or destroyed by a logout elsewhere.
	//
	// Renamed on the way to hesape: it is session.ErrExpired there. The alias
	// is what keeps github.com/arandu-io/kv correct without a line changing --
	// it returns this value, and hesape/session.Handler requires that one, and
	// they are the same value.
	ErrSessionExpired = session.ErrExpired

	// ErrConfirmationNotStored means the backend accepted the password
	// confirmation stamp and did not keep it, so no window would ever be
	// satisfied and the password screen would ask again immediately.
	ErrConfirmationNotStored = session.ErrConfirmationNotStored
)

Errors returned by SessionStore.

ErrCSRF means the token is invalid, expired, or bound to another session.

Renamed on the way to hesape: it is session.ErrTokenMismatch there.

View Source
var ErrExpired = encryption.ErrExpired

ErrExpired is a valid signature that has run out of time. It unwraps to ErrSignature, so a caller that does not care about the difference does not have to look at it.

View Source
var ErrForbidden = auth.ErrForbidden

ErrForbidden is the only authorization error. Handlers translate it to 403.

View Source
var ErrInvalidPassword = hashing.ErrInvalidPassword

ErrInvalidPassword means the password does not match the stored hash.

View Source
var ErrSignature = encryption.ErrSignature

ErrSignature is what every signature failure unwraps to, so a caller answers "this link is not valid" once rather than switching on four reasons it is not.

Functions

func HashPassword

func HashPassword(plain string) (string, error)

HashPassword returns the hash in PHC string format, which is self-describing and therefore allows rehashing when parameters change.

Renamed on the way to hesape: it is hashing.Make there.

func LocalPath added in v0.25.4

func LocalPath(raw string) (string, bool)

LocalPath reports whether an address stays inside this application, and returns it when it does.

It is the open-redirect defence, and it is one function in the collection rather than one per caller: http.Reject calls it on the address a rejected form is sent back to, which comes off the Referer header and is therefore the visitor's to choose, and the intended destination calls it twice around a signed cookie.

func NeedsRehash

func NeedsRehash(encoded string) bool

NeedsRehash reports that the hash was produced with older parameters, so the caller should rehash on the next successful login.

func PasswordConfirmedWithin added in v0.26.0

func PasswordConfirmedWithin(sub Subject, window time.Duration) bool

PasswordConfirmedWithin reports whether the password was typed again on this session less than window ago.

It was a method on Subject and it cannot be one here: Subject is an alias for hesape/auth.Subject, and Go does not let a package declare a method on a type another package owns. In hesape the question is asked of the session record -- session.Record.PasswordConfirmedWithin -- because that is where the stamp lives once the payload is opaque to the store.

So this is a function, and it is the one place in this bridge where a caller has to be rewritten rather than recompiled:

sub.PasswordConfirmedWithin(w)  becomes  security.PasswordConfirmedWithin(sub, w)

It builds the record hesape asks and asks it, so the three refusals -- no stamp, a stamp in the future, a window of zero -- are decided by the code that runs in production and not by a second copy of them here.

func ValidTenant added in v0.10.0

func ValidTenant(tenant string) bool

ValidTenant reports whether a tenant identifier is safe to use as a namespace.

func VerifyPassword

func VerifyPassword(plain, encoded string) error

VerifyPassword compares in constant time.

Renamed on the way to hesape: it is hashing.Check there, which also reads back argon2i and bcrypt -- what an imported users table holds.

Types

type Action

type Action = auth.Action

Action is the intended operation, in "module.verb" form.

type CSRF

type CSRF = session.CSRF

CSRF issues double-submit tokens signed with HMAC and bound to the session. It keeps no server-side state: the token carries its own expiry, so a deployment does not need Redis just to protect forms.

func NewCSRF

func NewCSRF(appKey []byte, ttl time.Duration) *CSRF

NewCSRF returns a token issuer keyed by the application key.

type Flash added in v0.25.4

type Flash = session.Flash

Flash carries the messages and the input of a rejected request across the one redirect that follows it.

func NewFlash added in v0.25.4

func NewFlash(appKey []byte, secure bool) *Flash

NewFlash returns a Flash over the application key.

type Grant

type Grant = auth.Grant

Grant is the proof that an authorization decision happened.

THIS IS THE CENTRAL PIECE OF THE FRAMEWORK, and the alias is what keeps it one type rather than two: a Grant minted by hesape/auth is the same value a repository written against this package checks.

func Authorize

func Authorize[T any](ctx context.Context, p Policy[T], s Subject, a Action, resource T) (Grant, error)

Authorize runs the policy and, when allowed, issues the Grant.

A wrapper and not an alias: Go has no alias form for a generic function.

func SystemGrant

func SystemGrant(a Action, tenant string) Grant

SystemGrant exists for jobs that run outside a request, and for the login path, where there is no subject yet.

A wrapper and not a var, deliberately: an exported function variable holding the framework's one escape hatch is a value any package in the build can reassign at init, and this is the last symbol in the collection that should be reachable that way.

type MemoryBackend

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

MemoryBackend keeps sessions in the process memory.

It is the right choice for development and for a single instance. Behind more than one pod it silently logs people out on every deploy and on every request routed elsewhere -- use the kv adapter there.

It is the same four renames as backendHandler, run the other way: the store underneath is hesape/session.ArrayHandler, and nothing is kept here.

func NewMemoryBackend

func NewMemoryBackend() *MemoryBackend

NewMemoryBackend returns an empty in-memory session backend.

func (*MemoryBackend) Delete

func (m *MemoryBackend) Delete(ctx context.Context, id string) error

Delete removes the session, if present.

func (*MemoryBackend) DeleteSubject added in v0.25.0

func (m *MemoryBackend) DeleteSubject(ctx context.Context, tenant, subjectID, keepID string) error

DeleteSubject removes every session of one subject of one tenant except keepID. An empty tenant or an empty subject id is refused.

func (*MemoryBackend) Get

func (m *MemoryBackend) Get(ctx context.Context, id string) (Subject, error)

Get returns the subject, or ErrSessionExpired when the id is unknown.

func (*MemoryBackend) Put

func (m *MemoryBackend) Put(ctx context.Context, id string, s Subject, ttl time.Duration) error

Put stores the subject under id for the given ttl.

type MemoryThrottle added in v0.25.0

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

MemoryThrottle is the sign-in throttle in process memory.

It is right for development and for a single instance. Behind more than one pod the budget multiplies by the number of pods -- use the kv-backed implementation there.

It is an envelope over hesape/auth.MemoryThrottle, which is the code that counts. What this adds is the context the old three methods do not take: the hesape implementation ignores it, because nothing in it blocks, so the one passed here is Background and the behaviour is unchanged.

func NewMemoryThrottle added in v0.25.0

func NewMemoryThrottle() *MemoryThrottle

NewMemoryThrottle returns an empty in-memory sign-in throttle.

func (*MemoryThrottle) Attempt added in v0.25.0

func (m *MemoryThrottle) Attempt(tenant, identity, client string) (time.Duration, bool)

Attempt takes one unit from the pair's budget and one from the address's, and reports whether the attempt may go ahead.

func (*MemoryThrottle) Clear added in v0.25.0

func (m *MemoryThrottle) Clear(tenant, identity, client string)

Clear forgets the pair's failures and refunds the address for this attempt.

func (*MemoryThrottle) Len added in v0.25.0

func (m *MemoryThrottle) Len() int

Len reports how many counters are held. It exists so a test can prove the table does not grow without bound.

func (*MemoryThrottle) Refund added in v0.25.0

func (m *MemoryThrottle) Refund(tenant, identity, client string)

Refund gives one unit back to each of the two budgets.

type Policy

type Policy[T any] = auth.Policy[T]

Policy decides. One policy per entity, always in the module's <entity>.policy.go file.

Generic types alias, so a policy written against this name satisfies hesape/auth.Policy without being rewritten.

type SessionBackend

type SessionBackend interface {
	// Get returns the subject, or ErrSessionExpired when the backend does not
	// hold the id -- expired, evicted, or destroyed by a logout elsewhere.
	Get(ctx context.Context, id string) (Subject, error)

	// Put stores the subject under id for the given ttl. It must store every
	// exported field of the Subject it is given: a field it silently drops
	// reads back as the zero value, and only in the deployment that uses that
	// backend.
	Put(ctx context.Context, id string, s Subject, ttl time.Duration) error

	// Delete removes the session, if present.
	Delete(ctx context.Context, id string) error

	// DeleteSubject removes every session belonging to one subject of one
	// tenant, except keepID. An empty keepID keeps none of them.
	//
	// The tenant is part of the question, not a filter applied afterwards.
	// An empty tenant or an empty subject id is an error.
	DeleteSubject(ctx context.Context, tenant, subjectID, keepID string) error
}

SessionBackend stores the subject behind a session id.

It stays declared here, with the old four method names, rather than aliasing hesape/session.Handler -- which renamed all four: Get is Read, Put is Write, Delete is Destroy and DeleteSubject is DestroyIndex.

github.com/arandu-io/kv implements this interface, by these names, and it is a separate module: an alias here would compile in the framework and break the adapter silently, which is the one failure this bridge exists to prevent. backendHandler is what carries an implementation of this across to hesape.

type SessionOption added in v0.25.0

type SessionOption = session.Option

SessionOption adjusts how a session is started.

Renamed on the way to hesape: it is session.Option there.

func Remember added in v0.25.0

func Remember(on bool) SessionOption

Remember asks for a session that survives closing the browser, for RememberLifetime instead of the store's ttl.

type SessionStore

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

SessionStore issues and validates sessions.

It is an envelope over hesape/session.RecordStore[Subject] and hesape/http.Intended, because the design diverged in three ways at once: four methods were renamed, Load's return type changed from a Subject to a Record that wraps one, and the intended destination left the session package altogether -- it is an address, and hesape/session never validates a URL.

The cookie is unchanged. hesape/session signs the same name with the same key, so a browser holding a session issued by the old code is still signed in.

func NewSessionStore

func NewSessionStore(appKey []byte, ttl time.Duration, secure bool, b SessionBackend) *SessionStore

NewSessionStore returns a store. Pass secure=false only in development: without the Secure attribute the cookie travels over plain HTTP.

func (*SessionStore) Confirm added in v0.25.0

Confirm records on the request's session that the subject has just typed their password again.

It returns ErrConfirmationNotStored when the backend accepted the stamp and did not keep it. A handler must report that rather than redirect: redirecting sends the person back to the screen they just got right.

func (*SessionStore) Destroy

func (s *SessionStore) Destroy(ctx context.Context, w http.ResponseWriter, id string) error

Destroy removes the session and clears the cookies this store put in the browser -- the session, and the intended destination.

The second one is not tidiness. Signing out is the moment a shared machine changes hands, and the intended address outlives it by up to IntendedLifetime: whoever signs in next is carried to the page the previous person was refused.

The two halves are two calls now, because the address moved to hesape/http: RecordStore.Invalidate ends the session and hhttp.Intended.Clear drops the address. Keeping them together here is what stops the second one from being forgotten at each call site.

Renamed on the way to hesape: it is RecordStore.Invalidate there.

func (*SessionStore) DestroyOthers added in v0.25.0

func (s *SessionStore) DestroyOthers(ctx context.Context, sub Subject, keepID string) error

DestroyOthers signs the subject out of every session except keepID.

The tenant comes from the subject, which came from the Grant or the session, never from the request. A subject with no tenant is refused.

func (*SessionStore) IDFromRequest

func (s *SessionStore) IDFromRequest(r *http.Request) string

IDFromRequest returns the session id when the cookie signature is valid, and the empty string otherwise. It is the function to hand to the CSRF middleware, which binds its token to this id.

Renamed on the way to hesape: it is RecordStore.ID there.

func (*SessionStore) Load

func (s *SessionStore) Load(ctx context.Context, r *http.Request) (Subject, error)

Load returns the subject bound to the request's session cookie.

Renamed on the way to hesape, and its return type changed with it: RecordStore.All answers a Record that wraps the payload with the tenant, the account and the two fields that decide the lifetime. This unwraps it.

func (*SessionStore) RememberIntended added in v0.25.0

func (s *SessionStore) RememberIntended(w http.ResponseWriter, r *http.Request)

RememberIntended records where this request was going, so that the sign-in screen it is about to be sent to can finish the journey.

Moved on the way to hesape: it is hhttp.Intended.Remember there, wired once at boot beside the store rather than hanging off it. This store builds one over the same application key, so the two are interchangeable in a browser.

func (*SessionStore) Rotate

func (s *SessionStore) Rotate(ctx context.Context, w http.ResponseWriter, oldID string, sub Subject, opts ...SessionOption) (string, error)

Rotate issues a new session id for the same subject and destroys the old one.

It MUST be called on login: keeping the pre-login id is session fixation. Renamed on the way to hesape: it is RecordStore.Regenerate there.

func (*SessionStore) Start

func (s *SessionStore) Start(ctx context.Context, w http.ResponseWriter, sub Subject, opts ...SessionOption) (string, error)

Start creates a session for the subject and writes the cookie.

func (*SessionStore) TakeIntended added in v0.25.0

func (s *SessionStore) TakeIntended(w http.ResponseWriter, r *http.Request, fallback string) string

TakeIntended returns the address RememberIntended stored, and clears it.

Moved on the way to hesape: it is hhttp.Intended.Take there.

type SignInThrottle added in v0.25.0

type SignInThrottle interface {
	// Attempt records one sign-in attempt against this identity from this
	// address and reports whether it may go ahead. A refused attempt costs
	// nothing, so hammering a locked-out account does not extend the lockout.
	//
	// The tenant is part of the key: a rate limit shared across tenants is one
	// customer's traffic locking another customer's users out.
	Attempt(tenant, identity, client string) (retryAfter time.Duration, ok bool)

	// Refund gives back the unit Attempt took, for an attempt that never
	// reached the credential. It gives back one unit and never clears the
	// count, which is what keeps it from being the way out of a lockout.
	Refund(tenant, identity, client string)

	// Clear forgets what this identity failed from this address, and gives the
	// address back the unit this attempt took. Call it on a successful sign-in.
	//
	// The address's remaining count is deliberately left standing: one account
	// whose password the caller does know is exactly what a script walking a
	// stolen list has.
	Clear(tenant, identity, client string)
}

SignInThrottle counts sign-in attempts, so that a leaked password list cannot be tried against an account faster than a person can type.

The unit is taken before the password is checked, in one indivisible step, and the two calls below give it back. Reading a counter and writing it back in two round trips reopens the hole this closes; the full argument is on hesape/auth.SignInThrottle.

It stays declared here rather than aliasing hesape/auth.SignInThrottle, which added a context.Context as the first parameter of all three methods. An alias would change the shape every caller in the framework and in the thirteen repositories that import it is written against, and a bridge that changes a signature is not a bridge.

type Signer added in v0.19.0

type Signer = encryption.Signer

Signer issues links that prove something without storing anything.

It is what an e-mail verification link is made of. The purpose and the expiry are both signed, which is what makes it safe to put in a URL.

func NewSigner added in v0.19.0

func NewSigner(appKey []byte) *Signer

NewSigner returns a Signer over the application key.

type Subject

type Subject = auth.Subject

Subject is whoever is acting. It comes from the session, never from the request body.

PasswordConfirmedWithin is no longer a method on it: see the package-level function of that name, and the note there for why.

func Guest added in v0.16.0

func Guest(tenant string) Subject

Guest is a reader with no session, declared on purpose.

Jump to

Keyboard shortcuts

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