syncauth

package
v0.10.1 Latest Latest
Warning

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

Go to latest
Published: Oct 24, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package syncauth is the one definition of how KySignOn signs a directory-sync event and how a product verifies it. KySignOn calls Sign; every receiver installs Middleware. Before this package the sender signed timestamp+"."+body and at least one receiver verified body alone, so the two sides could not agree, and a receiver that accepts a forged deprovision cannot tell it from a real one.

Scheme v1: HMAC-SHA256 over

"ky-sync-v1\n" + timestamp(RFC3339, UTC) + "\n" + eventType + "\n" + eventID + "\n" + hex(sha256(body))

carried in X-KySignOn-Signature as "v1=" + hex(mac), with X-KySignOn-Timestamp, X-KySignOn-Event-Type and X-KySignOn-Event-ID alongside. The HMAC key is the sync secret and travels nowhere: no Authorization header carries it. Event type and ID are bound so a captured event cannot be replayed as a different type, and the ID is what the receiver's replay guard remembers.

Index

Constants

View Source
const (
	HeaderSignature = "X-KySignOn-Signature"
	HeaderTimestamp = "X-KySignOn-Timestamp"
	HeaderEventType = "X-KySignOn-Event-Type"
	HeaderEventID   = "X-KySignOn-Event-ID"

	// DefaultWindow is how far a timestamp may sit from the receiver's clock. Homelab clocks
	// drift; five minutes is the usual bound and the error names which side is off.
	DefaultWindow = 5 * time.Minute
	// DefaultMaxBody bounds what a receiver reads before verifying. A directory event with
	// a few thousand users fits in far less.
	DefaultMaxBody = 4 << 20
	// MinKeyBytes is the shortest sync secret accepted. KySignOn mints 32 random bytes.
	MinKeyBytes = 16
)

Variables

View Source
var (
	ErrNoSignature  = errors.New("syncauth: no signature")
	ErrBadSignature = errors.New("syncauth: signature does not match")
	ErrBadTimestamp = errors.New("syncauth: timestamp missing or not RFC3339")
	ErrStale        = errors.New("syncauth: timestamp outside the accepted window")
	ErrReplay       = errors.New("syncauth: event already seen")
	// ErrReplayGuardFull means the in-memory guard holds max IDs all still inside the window
	// and cannot admit another without forgetting one it must remember. The receiver logs
	// that the guard is undersized; a persistent Replay backed by its processed-events table
	// is the fix for sustained volume.
	ErrReplayGuardFull = errors.New("syncauth: replay guard is full of events still inside the window")
	ErrMissingFields   = errors.New("syncauth: event type and event id are required")
	ErrShortKey        = fmt.Errorf("syncauth: key must be at least %d bytes", MinKeyBytes)
	ErrBodyTooLarge    = errors.New("syncauth: body exceeds the limit")
)

Functions

func Middleware

func Middleware(keyFn func(r *http.Request) ([]byte, error), o Options, maxBody int64, onReject func(r *http.Request, err error)) func(http.Handler) http.Handler

Middleware reads a bounded body, verifies it, and passes the request on with the same bytes replaced on r.Body so the handler decodes exactly what was signed. keyFn resolves the sync secret for this request (one per deployment, or per sender); an error from it is a 401 too, so an unconfigured receiver fails closed. Failures answer 401 with a fixed body; the reason goes only to onReject, for the receiver's log.

Types

type Event

type Event struct {
	Type string
	ID   string
	At   time.Time
}

Event is what a verified request says about itself.

func EventFromContext

func EventFromContext(r *http.Request) (Event, bool)

EventFromContext returns the verified Event Middleware stored.

func Verify

func Verify(key []byte, h Headers, body []byte, o Options) (Event, error)

Verify checks the headers against the body and the key. Every failure is one of the exported errors; the receiver answers 401 to all of them and logs which.

type Headers

type Headers struct {
	Signature string
	Timestamp string
	EventType string
	EventID   string
}

Headers are the four headers a signed request carries.

func FromRequest

func FromRequest(r *http.Request) Headers

FromRequest reads the headers off a request.

func Sign

func Sign(key []byte, at time.Time, eventType, eventID string, body []byte) (Headers, error)

Sign produces the headers for one event. eventType and eventID must be non-empty and free of newlines; eventID is stable across retries of the same event, so a receiver that saw the request but whose response was lost recognises the replay instead of acting twice.

func (Headers) Apply

func (h Headers) Apply(r *http.Request)

Apply sets the headers on a request.

type Options

type Options struct {
	// Window bounds |now - timestamp|. Zero means DefaultWindow.
	Window time.Duration
	// Now is the receiver's clock; nil means time.Now.
	Now func() time.Time
	// Replay, when set, is asked whether the event ID was already accepted inside the
	// window; a receiver with a table supplies its own, others use NewMemoryReplay.
	Replay Replay
}

Options tunes Verify.

type Replay

type Replay interface {
	Check(id string, at time.Time) error
}

Replay remembers accepted event IDs. Check returns nil and records id when it is new, ErrReplay when it was already recorded, or another error when it cannot decide (a guard that is full, a table that is down); any error refuses the event. Implementations must be safe for concurrent use.

func NewMemoryReplay

func NewMemoryReplay(window time.Duration, max int) Replay

NewMemoryReplay returns a Replay that remembers up to max IDs for window. Use it when the receiver has no table of processed events; a restart forgets everything, which is the window's job to bound. Size max above the events one sender can emit in a window, or the guard refuses under load.

Jump to

Keyboard shortcuts

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