webcore

package
v0.17.0 Latest Latest
Warning

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

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

Documentation

Overview

Package webcore holds the HTTP plumbing shared by DMCN's web-facing services (the dmcn-web mail client and the dmcn-b2c funnel service): session tokens, auth/CORS/CSP/rate-limit middleware, JSON error responses, and the challenge-response nonce store. It deliberately contains no service-specific handlers or state.

Index

Constants

View Source
const ContextKeyAddress contextKey = "address"

ContextKeyAddress is the context key under which the authenticated user's address is stored after successful session validation.

Variables

View Source
var (
	// ErrSessionNotFound is returned when a token is malformed or its signature
	// does not verify.
	ErrSessionNotFound = errors.New("webcore: session token invalid")
	// ErrSessionExpired is returned when a token's exp claim is in the past.
	ErrSessionExpired = errors.New("webcore: session expired")
	// ErrSessionRevoked is returned when a token was revoked (logged out) before
	// its natural expiry.
	ErrSessionRevoked = errors.New("webcore: session revoked")
)

Functions

func AddressFromContext

func AddressFromContext(ctx context.Context) string

AddressFromContext extracts the authenticated address from the request context. Returns an empty string if no address is present.

func AuthMiddleware

func AuthMiddleware(sessions *SessionStore) func(http.HandlerFunc) http.HandlerFunc

AuthMiddleware returns a middleware that validates the Bearer token from the Authorization header against the session store and injects the associated address into the request context.

func CORSMiddleware

func CORSMiddleware(devMode bool, allowedOrigins []string) func(http.Handler) http.Handler

CORSMiddleware returns a middleware that sets CORS headers. In dev mode all origins are allowed; in production the request Origin is echoed back when it is in allowedOrigins (each an "https://host" origin), otherwise the first entry is served as the default. Vary: Origin is set in production because the response header depends on the request.

func CSPMiddleware

func CSPMiddleware(cfg CSPConfig) func(http.Handler) http.Handler

CSPMiddleware returns a middleware that sets Content-Security-Policy and the companion security headers. It mints a per-request nonce, allows exactly that nonce in script-src (so the single inline runtime-config script runs while everything else stays no-unsafe-inline / no-eval), and stashes the nonce in the request context for the page handler to render.

Key custody is non-extractable + client-only (no server-held key blob), so XSS can't *exfiltrate* keys — but it could still drive sign/deriveBits while the page is open. These headers shrink that window: a tight CSP plus framing/sniffing/ referrer hardening (and HSTS in production).

func LoadOrCreateSecret

func LoadOrCreateSecret(path string) ([]byte, error)

LoadOrCreateSecret returns a persisted HMAC signing secret, generating and writing a new 32-byte random one (base64, 0600) if the file does not exist.

func NonceFromContext

func NonceFromContext(ctx context.Context) string

NonceFromContext returns the per-request CSP nonce set by CSPMiddleware, or "" if none is present. The SPA handler renders it into index.html so the inline runtime-config script can execute under the strict, no-unsafe-inline script CSP.

func RateLimitMiddleware

func RateLimitMiddleware(requestsPerMinute int) func(http.Handler) http.Handler

RateLimitMiddleware returns a middleware that limits requests per IP address to the specified number per minute using a sliding window.

func WriteError

func WriteError(w http.ResponseWriter, status int, msg string)

WriteError writes a JSON error response.

func WriteErrorCode

func WriteErrorCode(w http.ResponseWriter, status int, code, msg string)

WriteErrorCode writes a JSON error response carrying a stable machine-readable code alongside the human message, for clients that branch on the cause.

func WriteJSON

func WriteJSON(w http.ResponseWriter, status int, v interface{})

WriteJSON writes a JSON response with the given status code.

Types

type CSPConfig

type CSPConfig struct {
	// DevMode skips HSTS (avoid pinning localhost).
	DevMode bool
	// ExtraConnectSrc are additional origins allowed in connect-src.
	ExtraConnectSrc []string
	// FrameSelf adds 'self' to frame-src so the app can embed a SAME-ORIGIN sandboxed
	// iframe — the HTML-email renderer, a srcdoc frame with neither allow-scripts nor
	// allow-same-origin. Without it the frame is blocked outright by frame-src 'none'.
	FrameSelf bool

	// RemoteImages adds https: to img-src, which is what makes the reader's opt-in
	// "load remote images from senders you trust" possible at all.
	//
	// It has to live HERE, on the parent document, and that is worth being blunt about.
	// A srcdoc frame INHERITS the embedding document's CSP and the effective policy is
	// the intersection, so the renderer's own tighter policy can never widen what this
	// header allows — without this, the frame's img-src https: is silently overruled and
	// images fail with no signal the app can see.
	//
	// The ceiling is unconditional even though the feature is not: the preference lives
	// in the owner's sealed personal KV, which the server cannot read, so there is no
	// per-user header to emit. What stays conditional is enforcement, in two places the
	// server does not own — the sanitizer strips the src outright unless the reader opted
	// in AND the sender is allowlisted, and the frame's own meta CSP refuses the fetch on
	// top of that. This directive only stops being a third, unconditional blocker.
	//
	// The cost, stated plainly: post-XSS, img-src https: is a one-way beacon an attacker
	// can encode data into, which img-src 'self' data: denied. connect-src 'self' is
	// untouched, and the main document renders no untrusted HTML (all of it goes to the
	// sandboxed frame), so this needs script execution to reach. The fix that gives the
	// feature back without the ceiling is rendering mail on a SEPARATE origin, which
	// inherits nothing — real work, not a flag, and not done here.
	RemoteImages bool
}

CSPConfig tunes the Content-Security-Policy emitted by CSPMiddleware.

type ChallengeStore

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

ChallengeStore issues and tracks short-lived challenge nonces keyed by address, backing the challenge-response flows (mail-client login/import, the b2c account auth): the server mints a nonce, the browser signs it with the identity's Ed25519 key, and the server verifies against the registered public key.

func NewChallengeStore

func NewChallengeStore(ttl time.Duration) *ChallengeStore

NewChallengeStore creates a store whose nonces expire after ttl (default 60s when ttl <= 0).

func (*ChallengeStore) Delete

func (c *ChallengeStore) Delete(key string)

Delete consumes the key's outstanding nonce (call after successful verification).

func (*ChallengeStore) Get

func (c *ChallengeStore) Get(key string) ([]byte, bool)

Get returns the outstanding nonce for the key, or false when none is pending or it has expired (an expired entry is dropped). The nonce stays pending until Delete — a failed signature must not consume it, so the client can retry.

func (*ChallengeStore) Issue

func (c *ChallengeStore) Issue(key string) ([]byte, error)

Issue mints and stores a fresh 32-byte nonce for the key, replacing any outstanding one.

type SessionStore

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

SessionStore issues and validates stateless HS256 JWT session tokens. The happy path holds no per-session server state — tokens carry their own claims and survive restarts as long as the signing secret is stable. The only state is a small persisted denylist of tokens revoked (via logout) before they expire; entries are pruned once expired, since the token is invalid by then anyway.

func NewSessionStore

func NewSessionStore(secret []byte, ttl time.Duration, revokedPath string) (*SessionStore, error)

NewSessionStore creates a JWT session store. secret is the HMAC-SHA256 signing key (persist it so tokens survive restarts). ttl defaults to 24h when ≤ 0. revokedPath persists the revocation denylist; "" keeps it in-memory only.

func (*SessionStore) Create

func (s *SessionStore) Create(address string) (string, error)

Create issues a signed JWT for the address.

func (*SessionStore) Delete

func (s *SessionStore) Delete(token string)

Delete revokes a token (user logout): its jti is denylisted until the token's natural expiry, after which it is pruned. A no-op for an unverifiable token.

func (*SessionStore) Validate

func (s *SessionStore) Validate(token string) (string, error)

Validate verifies a token's signature and expiry and that it has not been revoked, returning the address (sub claim).

Jump to

Keyboard shortcuts

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