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 ¶
MinCSRFSharedKeyBytes is the shortest key CSRFConfig accepts.
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 ¶
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.
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.
var ErrInvalidCSRFConfig = errors.New("identity: invalid CSRF configuration")
ErrInvalidCSRFConfig is the class of every CSRFConfig rejection.
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.
var ErrInvalidPrincipal = errors.New("identity: invalid principal")
ErrInvalidPrincipal is the class of every NewPrincipal rejection.
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.
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.
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.
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 ¶
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 []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.
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 ¶
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.
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 ¶
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 ¶
IsService reports whether this principal may perform control-plane work.
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
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.
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.