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
- Variables
- func AddressFromContext(ctx context.Context) string
- func AuthMiddleware(sessions *SessionStore) func(http.HandlerFunc) http.HandlerFunc
- func CORSMiddleware(devMode bool, allowedOrigins []string) func(http.Handler) http.Handler
- func CSPMiddleware(cfg CSPConfig) func(http.Handler) http.Handler
- func LoadOrCreateSecret(path string) ([]byte, error)
- func NonceFromContext(ctx context.Context) string
- func RateLimitMiddleware(requestsPerMinute int) func(http.Handler) http.Handler
- func WriteError(w http.ResponseWriter, status int, msg string)
- func WriteErrorCode(w http.ResponseWriter, status int, code, msg string)
- func WriteJSON(w http.ResponseWriter, status int, v interface{})
- type CSPConfig
- type ChallengeStore
- type SessionStore
Constants ¶
const ContextKeyAddress contextKey = "address"
ContextKeyAddress is the context key under which the authenticated user's address is stored after successful session validation.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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.