Help us shape the future of Go! Take the 2026 Go Developer Survey

identity

package
v0.16.1 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: Apache-2.0 Imports: 14 Imported by: 2

Documentation

Overview

Package identity holds the tenancy vocabulary that Factory's composition seams exchange.

It is a public leaf package rather than an internal one for a reason that is a language constraint, not a preference: a Verifier or Authorizer supplied by a deployer must be able to NAME the type in its own method signatures, and Go forbids a package outside this module from importing github.com/looprig/factory/internal/... . Everything else about the seams stays with the package that calls them; see credential.go for why the authentication seam a deployer implements is the Verifier rather than an authenticator.

Index

Constants

View Source
const MinCSRFSharedKeyBytes = 32

MinCSRFSharedKeyBytes is the shortest key CSRFConfig accepts.

View Source
const RedactedPlaceholder = "REDACTED"

RedactedPlaceholder is what a scrubbed credential is replaced by. It is a fixed string rather than a length-preserving mask, because a mask discloses the length of the secret.

It is exported because it is the one piece of the redaction a reader outside this package has to name: internal/identity scrubs presented secrets out of a verifier's message with it, and there must be ONE such string rather than one per package that redacts.

Variables

View Source
var ErrClientPrincipal = errors.New("admission: principal is Factory-stamped; a client may not supply it")

ErrClientPrincipal refuses a principal supplied by a client. Only Factory may stamp the sender derived from verified credentials.

View Source
var ErrCredentialExpired = fmt.Errorf("%w: credential expired", ErrUnauthenticated)

ErrCredentialExpired is the expiry case of ErrUnauthenticated, and it WRAPS it, so a caller matching only ErrUnauthenticated needs no change.

It is named separately because it is the one rejection a browser can act on without a human: a live session whose credential aged out refreshes and the ClientLink reconnects, where an invalid credential must send the user back through login. Only whoever verified the credential knows which it was.

View Source
var ErrInvalidCSRFConfig = errors.New("identity: invalid CSRF configuration")

ErrInvalidCSRFConfig is the class of every CSRFConfig rejection.

View Source
var ErrInvalidOrigin = errors.New("identity: invalid origin")

ErrInvalidOrigin is the class of every ParseOrigin and CanonicalHostname rejection. A CSRFConfig rejection caused by one of its trusted origins wraps both this and ErrInvalidCSRFConfig.

View Source
var ErrInvalidPrincipal = errors.New("identity: invalid principal")

ErrInvalidPrincipal is the class of every NewPrincipal rejection.

View Source
var ErrMetadataUnsupported = errors.New("admission: the session's Host does not apply command principal or metadata")

ErrMetadataUnsupported refuses an attributed command for a resident Host that does not advertise support for principal and metadata.

View Source
var ErrRateLimited = errors.New("identity: rate limited")

ErrRateLimited is what an Authorizer or ObjectPolicy reports when a request is throttled and may be retried later. Implementations may wrap it without changing its classification.

View Source
var ErrUnauthenticated = errors.New("identity: unauthenticated")

ErrUnauthenticated is what an Authenticator reports when a request or ClientLink credential is absent, malformed, unverifiable, or expired.

It is public for the same language reason Principal is: the deployer's Authenticator is what decides this, and Factory's edge must be able to separate "this caller presented no valid credential", which is answered with a challenge, from a failure of the credential service, which is not the caller's fault and must not sign every user out.

View Source
var ErrUnauthorized = errors.New("identity: unauthorized")

ErrUnauthorized is what an Authorizer or ObjectPolicy reports when an authenticated principal may not perform an operation. Implementations may wrap it with an operation name, but must not add resource identifiers: a denial must not disclose whether a named resource exists.

Functions

func CanonicalHostname

func CanonicalHostname(authority string) (string, error)

CanonicalHostname canonicalizes an HTTP authority -- a Host header or an X-Forwarded-Host value -- to the same hostname form Origin.Hostname returns.

It is a SEPARATE entry point from ParseOrigin because the two grammars differ: an authority carries no scheme, so its port cannot be compared with a scheme's default and is validated and then discarded rather than normalized. Both entry points canonicalize the host through one implementation, so a host that is refused in one is refused in the other.

Discarding the port is what makes a host check a check on the NAME. A DNS rebinding attack rebinds a name; it does not choose the port the server is listening on. The port is not lost from the decision, because an inbound Origin header is compared as a whole origin, port included.

Types

type CSRFConfig

type CSRFConfig struct {
	// SharedKey signs CSRF tokens. It must be the same bytes in every replica.
	SharedKey []byte

	// TokenTTL bounds how long an issued token is accepted.
	TokenTTL time.Duration

	// TrustedOrigins is the set of browser origins allowed to open a
	// ClientLink or issue a state-changing request. Each entry is a bare
	// origin: scheme://host[:port], with no path, query, fragment or userinfo.
	//
	// Entries are compared in their CANONICAL form, not as written: see
	// ParseOrigin. Two entries that canonicalize alike are duplicates.
	TrustedOrigins []string

	// TrustForwardedHeaders makes a reverse-proxy deployment's
	// X-Forwarded-Host the host a guard checks, in place of the Host the
	// server itself received.
	//
	// It defaults to false and the default is the point. X-Forwarded-Host is an
	// ordinary request header: anything that can reach Factory's listener can
	// set it, so a guard that reads it unconditionally is a guard whose subject
	// the caller chooses. Enable it only where every route to Factory passes
	// through a proxy that OVERWRITES the header rather than appending to it.
	TrustForwardedHeaders bool
}

CSRFConfig is the browser-facing origin and CSRF configuration.

There is deliberately NO default that manufactures a key. A Factory replica owns only its local ClientLinks, and a browser may reconnect to a different replica or issue its next REST call there, so a key generated per process is state the next replica cannot verify. Requiring the operator to supply the key is what makes "stateless or shared" a configuration error rather than an intermittent production failure.

func (CSRFConfig) Clone

func (c CSRFConfig) Clone() CSRFConfig

Clone returns a copy that shares no memory with c.

bytes.Clone and slices.Clone both return nil for a nil input, so a zero CSRFConfig clones to a zero CSRFConfig rather than to one holding an empty key that Validate would have to distinguish from an absent one.

func (CSRFConfig) Validate

func (c CSRFConfig) Validate() error

Validate reports why this configuration may not be used.

type Claims

type Claims struct {
	// Tenant is the tenant scope the credential was issued for.
	Tenant sessionwire.TenantID
	// Subject identifies the caller within that tenant.
	Subject string
	// Kind separates a human actor from a Factory service identity.
	Kind Kind
	// ExpiresAt is when the credential stops being accepted. A credential with
	// no expiry is rejected: it is one that can never be logged out.
	ExpiresAt time.Time
}

Claims is what a verified credential asserts.

Tenant may be empty, and that is the local deployment's case rather than an error: a credential minted by a single-tenant local composition names no tenant and receives the configured default. See tenantFor for why that fallback is confined to an actor.

type Credential

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

Credential is presented authentication material on its way to a Verifier.

Its value is unexported and every rendering a consumer can reach is overridden, because the likeliest way a token reaches a log is that somebody formatted the value they were handed.

The four methods are NOT four independent mechanisms, and the difference was measured rather than assumed. Deleting GoString or MarshalJSON changes what %#v and encoding/json produce; deleting LogValue does not change what slog.TextHandler produces, because its KindAny path falls back to fmt and reaches String. LogValue is kept because slog.LogValuer is part of the type's contract -- a handler that reflects over the value rather than formatting it sees the difference -- and it is held by an interface assertion rather than by a rendering, since no rendering reads it.

Value is still readable, because a Verifier that cannot read the credential cannot verify it. The claim is that a Credential does not leak by ACCIDENT.

func NewCredential

func NewCredential(source Source, value string) Credential

NewCredential builds a credential presented at source.

func (Credential) GoString

func (c Credential) GoString() string

GoString renders the credential for %#v, which ignores String and would otherwise print every unexported field.

func (Credential) LogValue

func (c Credential) LogValue() slog.Value

LogValue renders the credential for log/slog, which would otherwise reflect over the struct rather than consult String.

func (Credential) MarshalJSON

func (c Credential) MarshalJSON() ([]byte, error)

MarshalJSON renders the credential for a structured encoder. encoding/json would emit {} today, since every field is unexported, but that is a property of the field set rather than a decision, and it says nothing to whoever reads the record.

func (Credential) Source

func (c Credential) Source() Source

Source reports where the credential was presented.

func (Credential) String

func (c Credential) String() string

String renders the credential for %v, %s and %q without its value.

func (Credential) Value

func (c Credential) Value() string

Value is the material a Verifier verifies.

type Kind

type Kind string

Kind separates a human actor from a Factory service identity. The cross-tenant reconciliation sweep is authorized for a service identity and for nothing else, so the distinction is part of the principal rather than a property a handler recomputes.

const (
	// KindActor is a principal acting for one tenant.
	KindActor Kind = "actor"
	// KindService is a Factory service identity performing control-plane work.
	KindService Kind = "service"
)

type Origin

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

Origin is a browser origin in the one form a guard may compare.

It exists because the previous shape check was not a normalization, and the difference is a guard that never matches rather than a guard that is loose: url.Parse lowercases the scheme and leaves the host exactly as written, so a configured "https://APP.Example.COM", an explicit ":443" and an empty ":" each validated and could then never equal the Origin a browser sends. This type is what makes BOTH SIDES of every comparison the same value: a configured entry and an inbound Origin header go through ParseOrigin, and what is compared is String.

Its fields are unexported and it holds no reference members, so it is comparable and a holder cannot widen it.

func ParseOrigin

func ParseOrigin(text string) (Origin, error)

ParseOrigin canonicalizes a browser origin: an inbound Origin header, or a CSRFConfig.TrustedOrigins entry.

What it NORMALIZES, each because a browser's own serialization already differs from what an operator is likely to write:

  • scheme and host case, since RFC 3986 makes both case-insensitive;
  • a port that is the scheme's default, which a browser omits;
  • an empty port, which "https://host:" is and no browser sends.

What it REFUSES rather than normalizes, and why refusing is the honest answer in each case:

  • a non-ASCII host. Mapping a Unicode host to its punycode form is IDNA, which this module does not implement and cannot approximate: an ASCII-only "normalization" of a Unicode host is a guess. A refusal names the problem at composition time; the alternative is an entry that validates and never matches.
  • a host with a TRAILING DOT. DNS resolves "app.example.com." and "app.example.com" to the same server, but a browser serializes the host as it appeared in the URL, so the two are DISTINCT origins to the same origin policy. Stripping the dot on both sides would therefore merge two origins the browser separates, which is a widening, not a normalization.
  • a port that is not a plain decimal 1-65535, so ":0443" -- which parses, and which no browser sends -- cannot become an entry that never matches.

The consequence for an INBOUND header is the same rule read the other way: an Origin carrying a trailing dot or a non-ASCII host cannot canonicalize, so it matches no entry and is rejected. Every refusal here is fail-closed.

func (Origin) Hostname

func (o Origin) Hostname() string

Hostname is the canonical host with no brackets and no port. It is what a host check compares, since the Host header a server receives names no scheme and therefore cannot be compared as an origin.

func (Origin) String

func (o Origin) String() string

String is the canonical serialization, and it is what a guard compares. Two origins are the same origin exactly when their String values are equal.

type Principal

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

Principal is an authenticated caller.

Every field is unexported and read through a method, so a handler holding one cannot widen its tenant or promote it to a service identity. That is the whole of the immutability claim: it is a value with no reference members, so a copy shares nothing with its original.

func NewPrincipal

func NewPrincipal(tenant sessionwire.TenantID, subject string, kind Kind) (Principal, error)

NewPrincipal builds a principal from verified credentials.

A rejection returns the ZERO Principal, not a partially built one: a caller that ignores the error must not end up holding a usable tenant scope.

func PrincipalFromWire added in v0.12.0

func PrincipalFromWire(wire sessionwire.Principal) (Principal, error)

PrincipalFromWire validates and converts a durable assertion; it does not freshly verify the recorded sender.

func (Principal) IsService

func (p Principal) IsService() bool

IsService reports whether this principal may perform control-plane work.

func (Principal) Kind

func (p Principal) Kind() Kind

Kind reports whether this is an actor or a service identity.

func (Principal) Subject

func (p Principal) Subject() string

Subject identifies the caller within its tenant.

func (Principal) Tenant

func (p Principal) Tenant() sessionwire.TenantID

Tenant is the tenant scope every later read and write is confined to.

func (Principal) Wire added in v0.12.0

func (p Principal) Wire() sessionwire.Principal

Wire returns the verified principal as Core's session wire record.

type RateLimitedError added in v0.15.0

type RateLimitedError struct {
	RetryAfter time.Duration
}

RateLimitedError reports a throttle with an optional RetryAfter duration. A positive duration is rounded up to whole seconds in HTTP's Retry-After header. A nonpositive duration leaves the header unset.

func (*RateLimitedError) Error added in v0.15.0

func (e *RateLimitedError) Error() string

func (*RateLimitedError) Unwrap added in v0.15.0

func (e *RateLimitedError) Unwrap() error

Unwrap lets errors.Is recognize ErrRateLimited through this error and its wrappers.

type Source

type Source string

Source names where a credential was presented. It is part of a diagnosable message: "the cookie credential was not verified" and "the bearer credential was not verified" are different operational problems.

const (
	// SourceBearer is an Authorization: Bearer header.
	SourceBearer Source = "bearer"
	// SourceCookie is the browser session cookie.
	SourceCookie Source = "cookie"
	// SourceLink is a ClientLink connect token.
	SourceLink Source = "link"
)

type Verifier

type Verifier interface {
	VerifyCredential(ctx context.Context, credential Credential) (Claims, error)
}

Verifier verifies a presented credential and reports what it asserts.

It is the seam through which "stateless or shared durably" is satisfied. This package keeps nothing between calls, so whatever a deployment uses to decide a credential -- a signature over a shared key, a token introspection endpoint, a shared session table -- is reachable from every Factory replica by construction.

An implementation reports a rejected credential by returning an error that wraps identity.ErrUnauthenticated. Any other error is treated as the verifier being unable to answer; see ErrVerifierUnavailable.

Jump to

Keyboard shortcuts

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