auth

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package auth holds TableX's authentication-adjacent primitives: CSRF token verification, login rate limiting, and the SSRF host guard for ad-hoc logins. Credential validation itself is just "open a pool with the user's input" and lives in the auth handler; this package provides the surrounding guards.

Index

Constants

View Source
const (
	CSRFHeader = "X-CSRF-Token"
	CSRFField  = "csrf_token"
)

CSRF header and form field names. htmx sends the token via an htmx:configRequest hook that sets the X-CSRF-Token header (app.js); plain (no-JS) forms carry it in a hidden input read from the form body.

Variables

This section is empty.

Functions

func CheckCSRF

func CheckCSRF(sessionToken, provided string) bool

CheckCSRF reports whether provided matches the session token in constant time.

func CheckHost

func CheckHost(ctx context.Context, host string, sec config.SecurityConfig) error

CheckHost enforces the SSRF controls from docs/security.md for ad-hoc logins: an optional allowlist/denylist and, when BlockPrivate is set, refusal to connect to loopback/private/link-local (incl. cloud metadata) addresses. SQLite (empty host) bypasses this — it has no network target.

func ClientIP

func ClientIP(r *http.Request, trust *ProxyTrust) string

ClientIP returns the client's IP. Shared by the access log and the login rate limiter so they key on the same value.

X-Forwarded-For is consulted only when the request arrived FROM a trusted proxy, and is then parsed right to left, skipping trusted hops: each proxy appends the address it received the request from, so the rightmost entry not in the trusted set is the real client. Anything left of that is attacker-controllable (the client can send its own X-Forwarded-For header), which is why the previous leftmost-entry behavior was spoofable and why an untrusted RemoteAddr means the header is ignored entirely.

func DialControl

func DialControl(sec config.SecurityConfig) func(network, address string, c syscall.RawConn) error

DialControl returns a net.Dialer.Control hook that enforces the same IP policy as CheckHost, but on the *resolved peer address* of each connection rather than on a pre-flight DNS lookup. CheckHost resolves and validates once at login; the driver then re-resolves when it actually dials, so a DNS-rebinding answer could otherwise slip a blocked address past the guard. Installing this on the dialer closes that TOCTOU window: link-local/metadata addresses are always refused, and private/loopback addresses are refused when BlockPrivate is set. It is attached (via ConnParams.DialControl) to ad-hoc network logins only; predefined servers are operator-trusted.

func SafeMethod

func SafeMethod(method string) bool

SafeMethod reports whether the HTTP method is read-only (no CSRF check needed).

func StateMatches

func StateMatches(stored, returned string) bool

StateMatches compares a returned state against the stored one in constant time, and refuses an empty stored state outright — an absent handshake means this browser never started a flow, so there is nothing a callback could be completing.

func TokenFromRequest

func TokenFromRequest(r *http.Request) string

TokenFromRequest extracts the CSRF token from the header first, then the form.

Types

type CIDRSet

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

CIDRSet is a parsed list of networks an address can be tested against. A nil set contains nothing, which is the safe reading for every caller here: an unconfigured list must never mean "everything".

func NewCIDRSet

func NewCIDRSet(setting string, cidrs []string) (*CIDRSet, error)

NewCIDRSet parses a CIDR list (bare IPs are accepted as /32 or /128), or returns nil when the list is empty. Invalid entries are rejected so a typo cannot silently widen or narrow the set; setting names the config key in the error, since the two callers are different keys.

func (*CIDRSet) Contains

func (s *CIDRSet) Contains(ip net.IP) bool

Contains reports whether ip falls in one of the parsed networks.

func (*CIDRSet) ContainsAddr

func (s *CIDRSet) ContainsAddr(addr string) bool

ContainsAddr is Contains for a textual address, which is the form both the access log and the metrics gate carry. An unparseable address is not in any set.

type Handshake

type Handshake struct {
	State    string
	Nonce    string
	Verifier string
}

Handshake is the per-attempt secret set. State and Nonce are compared on the way back; Verifier proves to the token endpoint that the code was redeemed by whoever started the flow.

func NewHandshake

func NewHandshake() (Handshake, error)

NewHandshake mints a fresh handshake. Each value is 256 bits of crypto/rand, base64url without padding — the PKCE code_verifier charset (RFC 7636 §4.1).

type Identity

type Identity struct {
	Subject string // the provider's stable "sub"
	Email   string
	Name    string
	// EmailVerified is the provider's email_verified claim, with absent
	// counting as false: on a self-service provider an UNVERIFIED email is an
	// attacker-choosable string, so any policy that matches on Email (the
	// sso.allowed_* lists) must require this to be true. Some providers never
	// send the claim (OneLogin; some Microsoft configurations) — such a
	// deployment reads as unverified, deliberately.
	EmailVerified bool
}

Identity is a verified person, as vouched for by the provider. It is deliberately not a database identity: TableX still needs the user's own credentials, and the audit trail keeps reporting the database account.

type OIDCProvider

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

OIDCProvider is a configured, discovered provider. Build one with NewOIDCProvider at startup; it is safe for concurrent use.

func NewOIDCProvider

func NewOIDCProvider(ctx context.Context, issuer, clientID, clientSecret, redirectURL string, scopes []string) (*OIDCProvider, error)

NewOIDCProvider fetches the provider's metadata and returns a ready provider.

Discovery happens once, at startup, and a failure is fatal to it: a gate that silently does not engage because its provider was unreachable at boot is worse than a server that refuses to start.

func (*OIDCProvider) AuthCodeURL

func (p *OIDCProvider) AuthCodeURL(h Handshake) string

AuthCodeURL is where the browser is sent to authenticate.

func (*OIDCProvider) Exchange

func (p *OIDCProvider) Exchange(ctx context.Context, code string, h Handshake) (Identity, error)

Exchange redeems an authorization code and returns the verified identity.

The handshake is the one this browser started, read from its session. Callers MUST have already compared the returned state against h.State — Exchange re-checks the nonce, which is the half that has to be verified against the token rather than against the redirect.

type ProxyTrust

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

ProxyTrust is the parsed trusted_proxy_cidrs set: the proxy addresses whose X-Forwarded-For entries may be skipped when resolving the real client IP. A nil/empty set trusts nothing — X-Forwarded-For is then never consulted.

It is a distinct type from the CIDRSet it holds because trusting a proxy is a different claim from matching an address, and one that must never be passed where the other is meant. The set is a named field rather than embedded, so Trusted stays the only way to ask — a promoted Contains would be a second name for the same question.

func NewProxyTrust

func NewProxyTrust(cidrs []string) (*ProxyTrust, error)

NewProxyTrust parses the configured trusted_proxy_cidrs list.

func (*ProxyTrust) Trusted

func (p *ProxyTrust) Trusted(ip net.IP) bool

Trusted reports whether ip belongs to a configured trusted-proxy range.

type RateLimiter

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

RateLimiter is a simple sliding-window counter used to throttle login attempts per key, mitigating brute force. Login uses three keys: the bare IP, (IP, username), and (IP, predefined-server). It is safe for concurrent use.

func NewRateLimiter

func NewRateLimiter(window time.Duration, max int) *RateLimiter

NewRateLimiter returns a limiter allowing at most max events per window. A max <= 0 disables limiting.

func (*RateLimiter) Reserve

func (rl *RateLimiter) Reserve(keys ...string) bool

Reserve atomically checks every key's remaining budget and, when all have room, records one attempt against each. The check and the recording share one critical section: with separate Allowed/Record calls, a concurrent burst of logins could all pass the check before any recording landed, exceeding the cap (check-then-act race). When any key is exhausted nothing is recorded and Reserve reports false. The login reservation is TWO-STAGE: the csrf middleware reserves the coarse bare-IP key before the body is parsed, and the Login handler reserves the identity keys — so each stage's keys are reserved atomically as a group (not across the two groups). Reset the identity keys on success; the bare-IP hit is deliberately retained.

func (*RateLimiter) Reset

func (rl *RateLimiter) Reset(key string)

Reset clears a key's history (call on successful login).

func (*RateLimiter) Sweep

func (rl *RateLimiter) Sweep()

Sweep prunes expired timestamps from every key, reclaiming memory for keys that are never touched again (e.g. one-off attacker-controlled IP|user combinations that would otherwise linger until next accessed). Call it periodically from a background goroutine.

Jump to

Keyboard shortcuts

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