auth

package
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Jun 5, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrCodeNotFound   = errors.New("pairing code not found")
	ErrCodeExpired    = errors.New("pairing code expired")
	ErrCodeConsumed   = errors.New("pairing code already consumed")
	ErrDeviceNotFound = errors.New("device not found")
)
View Source
var ErrInvalidSession = errors.New("invalid session")

Functions

func CSRFMiddleware

func CSRFMiddleware(next http.Handler) http.Handler

CSRFMiddleware implements double-submit cookie CSRF protection.

GET/HEAD/OPTIONS/TRACE: issues csrf_token cookie if absent, then passes. POST/PUT/PATCH/DELETE: compares the cookie with the X-CSRF-Token header (for HTMX / fetch callers) or the _csrf form field (for plain HTML forms); 403 on mismatch.

Exempt paths:

  • /auth and /auth/* (protected by one-time pairing token)
  • /api/* (programmatic CLI access via UNIX socket)

func DeviceIDFromContext

func DeviceIDFromContext(ctx context.Context) (string, bool)

DeviceIDFromContext returns the authenticated device ID stored in ctx by NewWebAuthMiddleware. Returns ("", false) for unauthenticated requests.

func GeneratePairingCode

func GeneratePairingCode() string

func HashCode

func HashCode(code string) []byte

func IsLoopback

func IsLoopback(r *http.Request) bool

IsLoopback reports whether the request came from a loopback address. UNIX domain socket connections (RemoteAddr == "" or "@") are also considered loopback since they are only reachable from the local machine.

If the request carries upstream-proxy headers (X-Forwarded-For, CF-Connecting-IP, Forwarded), the request is treated as NOT loopback even when RemoteAddr is 127.0.0.1 — because such a request arrived via a reverse proxy / tunnel (e.g. cloudflared forwarding to localhost:8080) and must not benefit from the bootstrap exemption granted to real local sessions. Header values themselves are not trusted (they can be spoofed); only the presence of the header is used as a "I came through a proxy" signal.

func NewWebAuthMiddleware

func NewWebAuthMiddleware(signer *SessionSigner, store *Store) func(http.Handler) http.Handler

NewWebAuthMiddleware returns middleware that enforces session cookie auth for web UI routes.

Exempt paths (pass through without any check): /login, /auth*, /static/*.

For non-exempt paths the logic is:

no cookie + loopback + no devices registered → warn + pass (bootstrap mode)
no cookie + anything else                    → 302 /login
invalid cookie                               → clear cookie + 302 /login
valid cookie                                 → pass (Verify updates last_seen, deviceID stored in ctx)

func WithDeviceID

func WithDeviceID(ctx context.Context, deviceID string) context.Context

WithDeviceID returns ctx with deviceID embedded. Used by NewWebAuthMiddleware and in tests to inject a device identity without running the full middleware.

Types

type ConnectionRegistry

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

ConnectionRegistry maps device IDs to the set of revoke channels for active long-lived connections (SSE / WebSocket). Calling RevokeDevice closes all channels registered for that device, causing handlers to unblock and return.

func NewConnectionRegistry

func NewConnectionRegistry() *ConnectionRegistry

func (*ConnectionRegistry) Register

func (r *ConnectionRegistry) Register(deviceID string) (<-chan struct{}, func())

Register records a long-lived connection for deviceID and returns a channel that is closed when the device is revoked, plus a release function that must be deferred by the caller to clean up when the connection ends normally.

func (*ConnectionRegistry) RevokeAll

func (r *ConnectionRegistry) RevokeAll()

RevokeAll closes all revoke channels for every registered device.

func (*ConnectionRegistry) RevokeDevice

func (r *ConnectionRegistry) RevokeDevice(deviceID string)

RevokeDevice closes all revoke channels registered for deviceID.

type Device

type Device struct {
	ID         string
	Label      string
	CookieHash []byte
	CreatedAt  time.Time
	LastSeenAt time.Time
	RevokedAt  *time.Time
}

type DeviceInfo

type DeviceInfo struct {
	ID         string    `json:"id"`
	Label      string    `json:"label,omitempty"`
	LastSeenAt time.Time `json:"last_seen_at"`
	CreatedAt  time.Time `json:"created_at"`
}

DeviceInfo is the API response representation of a paired web device.

type PairRequest

type PairRequest struct {
	Label string `json:"label,omitempty"`
}

PairRequest is the body for POST /api/web/pair.

type PairResponse

type PairResponse struct {
	Code      string    `json:"code"`
	URL       string    `json:"url,omitempty"`
	ExpiresAt time.Time `json:"expires_at"`
}

PairResponse is returned by POST /api/web/pair.

type PairingManager

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

func NewPairingManager

func NewPairingManager(store *Store) *PairingManager

func (*PairingManager) Issue

func (m *PairingManager) Issue(ctx context.Context, label string) (string, error)

func (*PairingManager) Redeem

func (m *PairingManager) Redeem(ctx context.Context, code string) (string, error)

type RateLimiter

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

func NewRateLimiter

func NewRateLimiter(now func() time.Time) *RateLimiter

func (*RateLimiter) Allow

func (rl *RateLimiter) Allow(ip string) bool

func (*RateLimiter) Allowed

func (rl *RateLimiter) Allowed(ip string) bool

Allowed reports whether ip is currently not rate-limited (read-only, no side effects).

func (*RateLimiter) RecordFailure

func (rl *RateLimiter) RecordFailure(ip string)

RecordFailure records a failed attempt for ip and locks it if the threshold is exceeded.

type SessionSigner

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

func NewSessionSigner

func NewSessionSigner(secret []byte, store *Store) *SessionSigner

func (*SessionSigner) Clear

func (s *SessionSigner) Clear(w http.ResponseWriter)

func (*SessionSigner) Issue

func (s *SessionSigner) Issue(w http.ResponseWriter, deviceID string) error

func (*SessionSigner) Verify

func (s *SessionSigner) Verify(r *http.Request) (string, error)

type Store

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

func NewStore

func NewStore(db *sql.DB) *Store

func (*Store) ConsumePairingCode

func (s *Store) ConsumePairingCode(ctx context.Context, codeHash []byte) (label string, err error)

func (*Store) DeleteRevokedDevices

func (s *Store) DeleteRevokedDevices(ctx context.Context, dryRun bool) (int64, error)

func (*Store) GetDevice

func (s *Store) GetDevice(ctx context.Context, id string) (*Device, error)

GetDevice returns the device with the given id, or nil if not found or revoked.

func (*Store) HasAnyDevice

func (s *Store) HasAnyDevice(ctx context.Context) (bool, error)

func (*Store) InsertDevice

func (s *Store) InsertDevice(ctx context.Context, id, label string, cookieHash []byte) error

func (*Store) InsertPairingCode

func (s *Store) InsertPairingCode(ctx context.Context, codeHash []byte, label string, expiresAt time.Time) error

func (*Store) ListDevices

func (s *Store) ListDevices(ctx context.Context) ([]*Device, error)

func (*Store) RevokeAllDevices

func (s *Store) RevokeAllDevices(ctx context.Context) error

func (*Store) RevokeDevice

func (s *Store) RevokeDevice(ctx context.Context, id string) error

func (*Store) UpdateDeviceLastSeen

func (s *Store) UpdateDeviceLastSeen(ctx context.Context, id string, t time.Time) error

Jump to

Keyboard shortcuts

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