Documentation
¶
Overview ¶
Package authgate holds framework-agnostic building blocks shared by every deepwork auth gate (the standalone terminal's net/http authWrap, the standalone teamworkbench gin gate, and deepwork-pro's gin middleware). It deliberately knows nothing about HTTP, gin, IPs or cookies — it is pure policy, so a single implementation serves every call site with no duplication (the SSOT for auth-code generation, canonicalization, comparison, and failure throttling).
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ForwardingHeaders = []string{
"X-Forwarded-For",
"X-Real-Ip",
"Forwarded",
"Cf-Connecting-Ip",
"True-Client-Ip",
}
ForwardingHeaders are set by any reverse proxy / tunnel in front of the server (cloudflared sets Cf-Connecting-Ip + X-Forwarded-For). Their mere PRESENCE means the request did NOT originate on this machine, so it disqualifies the localhost auth-bypass. They are NEVER read for access — they can only REVOKE the bypass, never grant it — so a forged header can't be used to get in.
Functions ¶
func CodeMatches ¶
CodeMatches reports whether provided equals expected after normalization, using a constant-time comparison so a remote attacker can't time-probe the code byte-by-byte (the mesh/tunnel widens who can guess at it). An empty expected never matches — an unset code must not be satisfiable by empty input.
func Generate ¶
func Generate() string
Generate returns a fresh human-friendly auth code: 8 chars grouped 4-4 with a hyphen (e.g. "E3X1-M6T2"). crypto/rand + rejection sampling keeps each pick uniform over the alphabet.
Tradeoff: ~39-bit entropy (30^8), far below a 128-bit hex token. That is fine for the local/LAN case, and safe enough for a public cloudflare tunnel ONLY because the failure Throttle bounds a network brute-force to an infeasible rate. An operator who wants more can set a custom code (longer/stronger) via the rotate path — NormalizeCode accepts any letters/digits.
func IsGenuineLocal ¶ added in v0.9.0
IsGenuineLocal reports whether a request truly originated on this machine and therefore earns the no-code convenience. "Genuine" means BOTH: the real TCP peer is a loopback address, AND no proxy/forwarding header is present.
This is the SSOT for the tunnel auth-bypass fix: a cloudflare tunnel reaches the server over loopback (cloudflared connects to 127.0.0.1), so the peer address alone can't tell tunnel traffic from a real local browser — and a framework's ClientIP() is worse, since it honours an attacker-set X-Forwarded-For. Callers pass the UN-spoofable TCP peer (net/http Request.RemoteAddr, gin c.Request.RemoteAddr) and a header accessor; the presence of ANY forwarding header proves the request came through a proxy/tunnel → not local → must auth.
authgate.IsGenuineLocal(c.Request.RemoteAddr, c.GetHeader) // gin authgate.IsGenuineLocal(r.RemoteAddr, r.Header.Get) // net/http
func NormalizeCode ¶
NormalizeCode canonicalizes an auth code for comparison: upper-cases it and drops every hyphen and whitespace rune. The display form is grouped for readability (e.g. "E3X1-M6T2"), but a user may type it lowercase, without the dash, or with a stray space — all denote the same code.
This drops NO entropy: the generated alphabet is uppercase A–Z/2–9 (no spaces or hyphens), so the separators and case carry no information.
SSOT: this is the ONE canonical form. Every auth boundary — the standalone terminal authWrap, the standalone teamworkbench gate, and deepwork-pro's WebUI middleware — normalizes through here so they can never disagree on what a code means.
Types ¶
type Throttle ¶
type Throttle struct {
// contains filtered or unexported fields
}
Throttle is a process-wide (GLOBAL, not per-IP) brake on FAILED auth attempts.
Why global, not per-IP: the auto-generated auth code is short + human-friendly (~39-bit), and a public cloudflare tunnel reaches the server over loopback (cloudflared → 127.0.0.1), collapsing every visitor to one source address. Per-IP limiting is therefore both useless (all traffic looks identical) and forgeable (X-Forwarded-For is attacker-controlled), so only a single shared failure budget actually bounds a network brute-force.
Two invariants, both intentional:
- Only FAILED attempts are charged. A correct code is NEVER delayed — call Reset on success and Penalty only on a failed compare. Honest logins stay instant even while a flood is in flight.
- It never hard-locks. It adds increasing delay (and the caller surfaces Retry-After); it never refuses a correct code. A legitimate user is at worst slowed if THEY mistype — never locked out by someone else's guessing.
The failure score decays exponentially, so a few human typos cost nothing (they sit under the free burst and evaporate during the quiet that follows) while a sustained guessing flood is throttled to an infeasible steady-state rate.
func NewThrottle ¶
func NewThrottle() *Throttle
NewThrottle returns a ready-to-use Throttle backed by the real clock.
func (*Throttle) Penalty ¶
Penalty decays the running failure score, charges one failure, and returns how long THIS failed attempt's response should be delayed. Call it ONLY after a failed compare. A zero return means the attempt is still within the free burst (the caller should answer with a normal 401, no throttle).