Documentation
¶
Overview ¶
ref: go-kratos/kratos middleware/auth/auth.go — auth middleware pattern Adopted: middleware wrapping pattern, Claims extraction from context. Deviated: separate TokenVerifier and Authorizer interfaces (GoCell splits authn from authz); no dependency on specific JWT library at this layer.
Package auth: Authenticator contract.
An Authenticator inspects an *http.Request and returns one of three outcomes:
(p, true, nil) — credential present and valid; caller accepts the principal. (absentPrincipal(), false, nil) — credential absent; caller decides (fail-closed by default). (nil, false, err) — credential present but invalid; caller MUST reject.
Implementations are consumed directly (one Authenticator per mount point, e.g. the WebSocket upgrade slot); there is no fan-out combinator.
Package auth defines authentication and authorization interfaces for the GoCell framework: JWT issuing/verification, key management, service tokens, and HTTP middleware. Concrete implementations live in cells/accesscore or adapters/.
Index ¶
- Constants
- Variables
- func AuthMiddleware(clk clock.Clock, verifier IntentTokenVerifier, opts ...AuthOption) func(http.Handler) http.Handler
- func CheckOwner[T any](resource T, ownerID func(T) string, callerID string, code errcode.Code) error
- func CompileIdempotencyExempts(entries []string) (func(*http.Request) bool, error)
- func CompilePasswordResetExempts(entries []string) (func(method, urlPath string) bool, error)
- func GenerateRSAKeyPair() (*rsa.PrivateKey, *rsa.PublicKey, error)
- func GenerateServiceToken(ring *HMACKeyRing, callerCell, method, path, rawQuery string, ts time.Time) string
- func LoadKeysFromEnv() (privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey, err error)deprecated
- func LoadRSAKeyPairFromPEM(privPEM, pubPEM []byte) (*rsa.PrivateKey, *rsa.PublicKey, error)
- func Mount(mux cell.RouteHandler, r Route) error
- func NewBootstrapMiddleware(creds BootstrapCredentials, limiter BootstrapRateLimiter, ...) func(http.Handler) http.Handler
- func PasswordResetBlocked(p *Principal, exempt bool) bool
- func RequireAnyRole(ctx context.Context, roles ...string) errordeprecated
- func RequirePolicy(p Policy) (func(http.Handler) http.Handler, error)
- func RequireRole(authorizer Authorizer, roles ...string) func(http.Handler) http.Handler
- func RequireSelfOrRole(ctx context.Context, targetID string, bypassRoles ...string) errordeprecated
- func ServiceTokenMiddleware(ring kauth.HMACKeyring, clk clock.Clock, opts ...ServiceTokenOption) func(http.Handler) http.Handler
- func TestContext(subject string, roles []string) context.Context
- func TestServiceContext(callerCell string) context.Context
- func Thumbprint(pub *rsa.PublicKey) string
- func TypHeaderForIntent(intent TokenIntent) string
- func WithPrincipal(ctx context.Context, p *Principal) context.Context
- type AccountLockoutMetrics
- type AuthMetrics
- type AuthOption
- func WithLogger(l *slog.Logger) AuthOption
- func WithMetrics(m *AuthMetrics) AuthOption
- func WithPasswordResetChangeEndpointHintFn(fn func() string) AuthOption
- func WithPasswordResetExemptMatcher(fn func(method, urlPath string) bool) AuthOption
- func WithPublicEndpointMatcher(fn func(*http.Request) bool) AuthOption
- type Authenticator
- type AuthenticatorFunc
- type Authorizer
- type BootstrapAuthFailObserver
- type BootstrapCredentials
- type BootstrapRateLimiter
- type Claims
- type EnvKeyProvider
- type EnvKeyProviderOption
- type HMACKeyRing
- type InMemoryNonceOption
- type InMemoryNonceStore
- type IntentTokenVerifier
- type IssueOptions
- type JWTIssuer
- type JWTIssuerOption
- type JWTVerifier
- type JWTVerifierOption
- type KeyProvider
- type KeySet
- type KeySetOption
- type NonceStore
- type NonceStoreKind
- type NoopNonceStore
- type Policy
- type Principal
- type PrincipalKind
- type Route
- type ServiceTokenOption
- type SigningKeyProvider
- type StaticKeyProvider
- type TokenIntent
- type VerificationKey
- type VerificationKeyStore
Constants ¶
const ( // EnvJWTPrivateKey is the environment variable for the PEM-encoded RSA private key. EnvJWTPrivateKey = "GOCELL_JWT_PRIVATE_KEY" // EnvJWTPublicKey is the environment variable for the PEM-encoded RSA public key. EnvJWTPublicKey = "GOCELL_JWT_PUBLIC_KEY" // EnvJWTPrevPublicKey is the environment variable for the previous (verification-only) public key. EnvJWTPrevPublicKey = "GOCELL_JWT_PREV_PUBLIC_KEY" // EnvJWTPrevKeyExpires is the environment variable for the expiry of the previous key (RFC 3339). EnvJWTPrevKeyExpires = "GOCELL_JWT_PREV_KEY_EXPIRES" )
const ( // NonceStoreKindNoop is the explicit disable-replay-check sentinel. // Rejected in adapter mode "real" by cmd/corebundle.SharedDeps.Validate. NonceStoreKindNoop = kauth.NonceStoreKindNoop // NonceStoreKindInMemory is the single-process map-backed implementation. // Suitable for single-pod deployments; a shared store is required for // multi-pod replay protection. NonceStoreKindInMemory = kauth.NonceStoreKindInMemory // NonceStoreKindDistributed is reserved for shared backends (Redis, consul, // etc.). Production multi-pod deployments must use this kind. NonceStoreKindDistributed = kauth.NonceStoreKindDistributed )
const ( // EnvServiceSecret is the environment variable for the current HMAC secret. EnvServiceSecret = "GOCELL_SERVICE_SECRET" // EnvServiceSecretPrevious is the environment variable for the previous HMAC secret. EnvServiceSecretPrevious = "GOCELL_SERVICE_SECRET_PREVIOUS" )
const DefaultAccessTokenTTL = 15 * time.Minute
DefaultAccessTokenTTL is the default time-to-live for access tokens issued by JWTIssuer.
const MinHMACKeyBytes = kauth.MinHMACKeyBytes
MinHMACKeyBytes aliases kauth.MinHMACKeyBytes so runtime/auth and kernel/cell share a single canonical strength threshold (NIST SP 800-107: HMAC-SHA-256 requires ≥256-bit keys). Kept as a const for callers that reference the runtime/auth namespace; the source of truth lives in kernel/cell.
const MinRSAKeyBits = 2048
MinRSAKeyBits is the minimum RSA key size accepted by the auth package. Keys smaller than 2048 bits (e.g. 512 or 1024) are considered insecure and will be rejected during parsing and verifier/issuer construction.
const RoleAdmin = "admin"
RoleAdmin is the canonical role name for administrative privilege.
const ServiceTokenClockSkew = 30 * time.Second
ServiceTokenClockSkew is the maximum future timestamp skew accepted for service tokens. It is intentionally separate from ServiceTokenMaxAge: old tokens expire by age, while future-dated tokens are only tolerated for bounded clock drift.
const ServiceTokenMaxAge = 5 * time.Minute
ServiceTokenMaxAge is the maximum age of a service token before it is rejected. Tokens with timestamps at or beyond this window are refused.
const ServiceTokenNonceTTL = ServiceTokenMaxAge + ServiceTokenClockSkew
ServiceTokenNonceTTL is the required replay-retention window for nonce stores used with service tokens. It covers the full token validity window plus the accepted future clock skew.
const ( // TokenIntentAccess marks a short-lived credential for calling business // endpoints. Verifier rejects any access token replayed at /auth/refresh. TokenIntentAccess = kauth.TokenIntentAccess )
Variables ¶
var ErrKeyMissing = errcode.ErrAuthKeyMissing
ErrKeyMissing indicates a required JWT key environment variable is not set.
var ErrNonceReused = errcode.New(errcode.KindUnauthenticated, errcode.ErrAuthNonceReused, "auth: nonce already used")
ErrNonceReused is returned by NonceStore.CheckAndMark when a nonce has already been consumed within its TTL window.
*errcode.Error implements errors.Is via pointer equality — callers use errors.Is(err, ErrNonceReused) unchanged. Callers distinguish replay (ErrNonceReused in the chain → 401) from store infrastructure failure (any other Cause → 500) via errors.Is on this exact sentinel; see writeServiceTokenError in servicetoken.go. The HTTP error envelope is constructed at the middleware layer.
var ErrNonceStoreFull = errcode.New(errcode.KindUnavailable, errcode.ErrNonceStoreFull,
"auth: nonce store is full; no expired entries to reclaim")
ErrNonceStoreFull is returned by InMemoryNonceStore.CheckAndMark when the nonce map has reached maxEntries and a forced prune found no expired entries to reclaim. Callers should treat this as a transient infrastructure failure (503 / Requeue) rather than a replay signal.
Functions ¶
func AuthMiddleware ¶
func AuthMiddleware(clk clock.Clock, verifier IntentTokenVerifier, opts ...AuthOption) func(http.Handler) http.Handler
AuthMiddleware extracts a Bearer token from the Authorization header, verifies it using the provided IntentTokenVerifier (always enforcing token_use=access for business endpoints), and stores the resulting Claims in the request context. On failure, it returns a 401 JSON response.
The parameter is IntentTokenVerifier (not TokenVerifier) by design: the access-vs-refresh distinction is a hard safety invariant — any verifier plugged into business routes must be able to enforce it at the type level, so we refuse to compile call sites that pass an intent-unaware verifier.
Public-endpoint bypass is provided through AuthOption values (WithPublicEndpointMatcher). The Router installs this via a lazy closure during FinalizeAuth so that route declarations from all Cells are aggregated before the predicate is compiled.
Internal listener routes that authenticate via service-token or mTLS live on a physically separate listener+mux and never reach this middleware — no in-band bypass predicate is needed.
ref: go-kratos/kratos — public bypass via selector at composition layer ref: go-zero — JWT opt-in per route group, no hidden runtime defaults
func CheckOwner ¶
func CheckOwner[T any]( resource T, ownerID func(T) string, callerID string, code errcode.Code, ) error
CheckOwner verifies that callerID matches the owner of resource as determined by the ownerID accessor. It returns KindNotFound on mismatch or on empty callerID — never PermissionDenied — to avoid leaking resource existence to unauthorized callers (IDOR-safe 404 collapse).
Empty callerID is treated as a mismatch (fail-closed): the funnel itself rejects empty subjects so that an upstream auth bug (missing JWT subject, stale middleware, public-route misconfig) cannot silently grant access. The canonical caller pattern (sessionlogout.Service.Logout's empty callerUserID guard returning KindInvalid) is still required for proper 400 mapping at the handler boundary; CheckOwner's fail-closed check is the defense-in-depth backstop.
Accessor is responsible for nil-safety on pointer T: returning "" for a nil resource collapses lookup-failure into the same envelope as owner-mismatch, preserving the IDOR collapse property.
Details are intentionally absent from the returned envelope: adding ownerID or callerID to errcode.Details would leak existence information to 4xx clients and break the IDOR-safe collapse contract.
The wire message is the const literal "not found" (see checkOwnerMsg); resource-type semantics live in the caller-supplied errcode.Code only. This keeps the funnel within MESSAGE-CONST-LITERAL-01 without needing a carve-out and makes the IDOR collapse uniform: a session-not-found and a device-not-found both surface as `{"code": "<resource-code>", "message": "not found"}`.
INVARIANT: SERVICEOWNED-HANDLER-OWNER-CHECK-01 (Hard)
All service-layer ownership checks on serviceOwned contracts must go through CheckOwner. service.go in serviceOwned slices must not construct errcode.New(errcode.KindNotFound, ...) directly; archtest predicate B3 enforces this as a zero-tolerance ban.
ref: kubernetes/apiserver pkg/authorization/authorizer/interfaces.go (Decision.DenyOnly philosophy); ent privacy/privacy.go (typed sentinel for policy decisions).
func CompileIdempotencyExempts ¶
CompileIdempotencyExempts compiles a list of "METHOD /path" entries into a request-level predicate used by the idempotency middleware's WithExemptMatcher option. Path segments of the form {xxx} match any single non-empty URL segment (same template semantics as CompilePasswordResetExempts), e.g. "POST /api/v1/access/users/{id}/password".
The returned predicate accepts *http.Request and inspects r.Method and r.URL.Path — suitable for WithExemptMatcher.
Validation and error aggregation behavior mirrors CompilePasswordResetExempts.
func CompilePasswordResetExempts ¶
CompilePasswordResetExempts compiles a list of "METHOD /path" entries into a method-aware predicate used by WithPasswordResetExemptMatcher. Path segments of the form {xxx} match any single non-empty URL segment (mirroring the router's template semantics), e.g. "POST /api/v1/access/users/{id}/password".
Validation matches middleware.CompilePublicEndpoints fail-fast strength (reviewer finding — prior divergence allowed weaker exempt configs to slip through startup):
- entry must be in "METHOD /path" format, non-empty on both sides
- method must be one of GET/HEAD/POST/PUT/PATCH/DELETE/OPTIONS/CONNECT/TRACE
- path must start with '/'
- duplicate (method, template) pairs are rejected
Errors across the batch are aggregated via errors.Join so the operator sees every malformed entry in a single startup failure rather than whack-a-mole.
An empty input list returns a matcher that exempts nothing — the fail-closed default that middleware enforces before callers wire a matcher.
This helper exists so the composition root (main.go / bootstrap) owns the list of exempt endpoints — runtime/auth no longer hard-codes cell-specific paths (F6 decoupling).
func GenerateRSAKeyPair ¶
func GenerateRSAKeyPair() (*rsa.PrivateKey, *rsa.PublicKey, error)
GenerateRSAKeyPair generates a 2048-bit RSA key pair. Returns an error when the RNG fails (effectively never on normal systems, but the API is error-first so dev-mode composition roots can propagate the failure instead of panicking). Test code should prefer `runtime/auth/keystest` helpers which wrap this with the test-fixture convenience semantics.
Production callers must NOT rely on this for persistent keys — ephemeral keys invalidate all issued tokens on process restart. Use LoadKeySetFromEnv to load persistent RSA material from configuration.
func GenerateServiceToken ¶
func GenerateServiceToken(ring *HMACKeyRing, callerCell, method, path, rawQuery string, ts time.Time) string
GenerateServiceToken creates a service token for the given callerCell, method, path, optional rawQuery, and timestamp using the current secret from the key ring. The token format is "{timestamp}:{nonce}:{callerCell}:{hex_hmac}" where nonce is 16 cryptographically random bytes, hex-encoded. It returns an empty string if:
- ring is nil
- callerCell is empty (mandatory — identifies the originating cell)
- callerCell contains ':' (would corrupt the 4-part token structure)
rawQuery is canonicalized separately from path so the HMAC message remains stable across query parameter ordering. Pass "" when the request has no query parameters.
func LoadKeysFromEnv
deprecated
func LoadKeysFromEnv() (privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey, err error)
Deprecated: Use LoadKeySetFromEnv instead, which returns a KeySet with kid support and optional verification-only key loading.
LoadKeysFromEnv reads PEM-encoded RSA keys from environment variables GOCELL_JWT_PRIVATE_KEY and GOCELL_JWT_PUBLIC_KEY. It returns an errcode error if either variable is missing or contains invalid PEM/key data.
func LoadRSAKeyPairFromPEM ¶
LoadRSAKeyPairFromPEM parses PEM-encoded RSA private and public keys.
func Mount ¶
func Mount(mux cell.RouteHandler, r Route) error
Mount registers the Route on mux. It:
- Validates the Route shape (Contract required, Contract.Kind=="http", bypass-flag mutual exclusivity).
- Derives the chi-relative registration path by stripping mux.Prefix() from Contract.Path (when the mux implements cell.Prefixer) so chi's own prefix composition produces the correct external URL.
- Wraps Handler with RequirePolicy(Policy) (inner) and wrapper.HTTPHandler(Contract) (outer) — the wrapper writes ctxkeys.ContractID + contributes span attrs to the outer middleware Tracing's single span, so policy denials (403) still emit gocell.contract.id.
- Registers the handler on mux via Handle("{METHOD} {relPath}").
- Forwards AuthRouteMeta to cell.AuthRouteDeclarer if mux implements it (chiRouterAdapter composes the prefix back to fully-qualified path before handing off to the top-level Router's declaration table).
Mount returns a non-nil error on invalid configurations so callers (cell.RouteGroup.Register) can propagate the failure to bootstrap phase5 without aborting the program. Composition-root call sites that want fail-fast wiring can wrap the error in a panic at the top of the call chain; cells that consult runtime config should propagate the error instead.
func NewBootstrapMiddleware ¶
func NewBootstrapMiddleware( creds BootstrapCredentials, limiter BootstrapRateLimiter, onAuthFail BootstrapAuthFailObserver, ) func(http.Handler) http.Handler
NewBootstrapMiddleware constructs the HTTP middleware chain for bootstrap authentication. The chain is: RateLimit (per-IP) → Basic Auth header parse → constant-time username/password comparison → uniform 401 envelope on any mismatch (no field-level oracle).
onAuthFail is an optional observer invoked on every authentication failure (after the response is written). The reason string is one of: "missing_header", "wrong_credentials", "rate_limited". Callers use this hook to write audit log entries without importing cells/. Pass nil to disable.
Wire this middleware around the setup/admin handler to enforce D5 semantics: env credentials authenticate the operator; body credentials define the admin identity.
func PasswordResetBlocked ¶
PasswordResetBlocked reports whether a verified principal carrying password_reset_required must be rejected on a non-exempt operation. It operates on the VERIFIED *Principal (not forgeable claims); each transport renders its own rejection (HTTP 403 ERR_AUTH_PASSWORD_RESET_REQUIRED vs gRPC codes.PermissionDenied). A nil principal is treated as not-blocked (the caller has already handled the unauthenticated case).
func RequireAnyRole
deprecated
RequireAnyRole checks that the authenticated subject holds at least one of the specified roles. Returns nil on success.
Deprecated: prefer constructing auth.AnyRole(...) / auth.SelfOr(...) Policy and passing it via auth.Route.Policy. The internal Require* helpers below are retained only as the implementation backbone.
Use this instead of RequireSelfOrRole for admin-only endpoints where there is no target resource owner to compare against.
Calling with zero roles always returns ErrAuthForbidden (no role can match).
Errors:
- ErrAuthUnauthorized: no Principal in context, or PrincipalUser with empty Subject
- ErrAuthForbidden: subject does not hold any of the required roles
func RequirePolicy ¶
RequirePolicy lifts a Policy into a middleware-shaped `func(http.Handler) http.Handler`. On policy failure it writes the domain error via httputil.WriteError and short-circuits the chain; on success it delegates to next.
ref: grpc-ecosystem/go-grpc-middleware auth.UnaryServerInterceptor — policy is declared at registration time, not inside the handler body. ref: go-chi/jwtauth Authenticator — short-circuit pattern with response written inside the guard.
func RequireRole ¶
RequireRole checks that the authenticated subject has at least one of the specified roles. The AuthMiddleware must run before this middleware. On failure, it returns a 403 JSON response.
func RequireSelfOrRole
deprecated
RequireSelfOrRole checks that the authenticated subject matches targetID or holds one of the specified bypass roles. Returns nil on success.
Deprecated: prefer constructing auth.AnyRole(...) / auth.SelfOr(...) Policy and passing it via auth.Route.Policy. The internal Require* helpers below are retained only as the implementation backbone.
ref: go-kratos/kratos middleware/auth/auth.go — adopted: subject-from-context pattern; deviated: combined self+role check instead of separate authz middleware.
Errors:
- ErrAuthUnauthorized: no Principal in context, or PrincipalUser with empty Subject
- ErrAuthForbidden: subject does not match targetID and lacks bypass roles
func ServiceTokenMiddleware ¶
func ServiceTokenMiddleware(ring kauth.HMACKeyring, clk clock.Clock, opts ...ServiceTokenOption) func(http.Handler) http.Handler
ServiceTokenMiddleware validates requests using HMAC-SHA256 service tokens. The token is expected in the Authorization header as:
ServiceToken {unix_timestamp}:{nonce}:{hex_hmac}
The HMAC is computed over "{method} {path}[?{canonicalQuery}] {timestamp} {nonce}".
Verification tries each secret in the key ring in order (current, then previous). Tokens older than 5 minutes (exclusive boundary) are rejected.
Principal construction is fully delegated to NewServiceTokenAuthenticator so that the service identity shape is defined in a single place.
ring accepts kauth.HMACKeyring (the kernel interface); *HMACKeyRing satisfies it structurally and remains the canonical production implementation.
Misconfiguration paths (nil ring, sub-strength HMAC, missing/Noop NonceStore, authenticator build failure) return an error middleware that serves 500 on every request. All misconfiguration paths share the same errorMiddlewareInternal helper so the 500 behavior is consistent and observable via the "internal" metric label.
func TestContext ¶
TestContext creates a context carrying the given subject and roles for use in handler tests across cells/. Follows the net/http/httptest naming pattern.
This helper is NOT deprecated; it is the recommended way to inject a Principal in handler tests.
For service-principal tests, use TestServiceContext.
func TestServiceContext ¶
TestServiceContext creates a context carrying a service Principal with the given callerCell for use in handler/mount tests. Follows the net/http/httptest naming pattern. Use this instead of TestContext for tests that exercise internal endpoints protected by RequireCallerCell.
See also: TestContext for user-principal tests.
func Thumbprint ¶
Thumbprint computes the RFC 7638 JSON Web Key (JWK) Thumbprint of an RSA public key using SHA-256. The result is a 43-character base64url-encoded (no padding) string derived from the SHA-256 hash of the canonical JWK representation. The same key always produces the same thumbprint.
ref: RFC 7638 §3.2 — required members for RSA in lexicographic order: "e", "kty", "n"
func TypHeaderForIntent ¶
func TypHeaderForIntent(intent TokenIntent) string
TypHeaderForIntent is the exported form of jwtTypForIntent, intended for test harnesses in sibling packages that need to build synthetic JWTs whose JOSE typ header matches what VerifyIntent expects. Production code should not call this — JWTIssuer.Issue writes the correct typ header automatically.
Types ¶
type AccountLockoutMetrics ¶
type AccountLockoutMetrics struct {
// contains filtered or unexported fields
}
AccountLockoutMetrics is the standalone observability instrument for the auto-lockout transitions emitted by cells/accesscore/internal/accountlockout. It registers a single CounterVec (`auth_account_lockout_total`) and lives apart from AuthMetrics so the cell composition root can wire it independently of bootstrap's router-level auth metrics — the two use the same metrics.Provider but register disjoint metric names, avoiding the "already registered" error that would result from constructing AuthMetrics twice with the same provider.
func NewAccountLockoutMetrics ¶
func NewAccountLockoutMetrics(p metrics.Provider) (*AccountLockoutMetrics, error)
NewAccountLockoutMetrics registers the `auth_account_lockout_total` CounterVec for ACCESSCORE-ACCOUNT-LOCKOUT-AUTO-LOCK-01. The composition root wires this into AccessCore via WithLockoutMetrics; the cell hands the returned recorder to accountlockout.NewService.
reason label ∈ {"threshold_locked", "lazy_unlocked"}. "threshold_locked" — consecutive-failure threshold reached → user auto-locked. "lazy_unlocked" — locked_until elapsed; sessionlogin transparently reactivated the account inside the login tx (silent on the outbox, visible only via this counter + slog per ADR §"lazy-unlock 审计策略").
func (*AccountLockoutMetrics) IncAccountLockout ¶
func (m *AccountLockoutMetrics) IncAccountLockout(ctx context.Context, reason string)
IncAccountLockout implements accountlockout.MetricsRecorder. reason MUST be one of {"threshold_locked", "lazy_unlocked"}; arbitrary strings are recorded as-is, but the accountlockout package only emits the two values listed.
type AuthMetrics ¶
type AuthMetrics struct {
// contains filtered or unexported fields
}
AuthMetrics holds pre-registered metric instruments for auth operations.
func NewAuthMetrics ¶
func NewAuthMetrics(p metrics.Provider) (*AuthMetrics, error)
NewAuthMetrics registers auth metric instruments with the given provider.
type AuthOption ¶
type AuthOption func(*authConfig)
AuthOption configures auth middleware behavior.
func WithLogger ¶
func WithLogger(l *slog.Logger) AuthOption
WithLogger sets the logger for auth middleware.
func WithMetrics ¶
func WithMetrics(m *AuthMetrics) AuthOption
WithMetrics sets the AuthMetrics for auth middleware.
func WithPasswordResetChangeEndpointHintFn ¶
func WithPasswordResetChangeEndpointHintFn(fn func() string) AuthOption
WithPasswordResetChangeEndpointHintFn sets a getter closure that is called at request time to obtain the changePasswordEndpoint hint. Use this when the hint value is not known at middleware install time (e.g. it is derived by FinalizeAuth after RegisterRoutes completes).
When fn is nil the option is a no-op.
func WithPasswordResetExemptMatcher ¶
func WithPasswordResetExemptMatcher(fn func(method, urlPath string) bool) AuthOption
WithPasswordResetExemptMatcher installs a (method, path) → bool predicate used by the password-reset gate. When a token carries password_reset_required=true, only requests for which the matcher returns true are allowed through; everything else returns 403.
If no matcher is supplied, the gate rejects every authenticated request — fail-closed. Composition roots must opt in to exempt paths (typically the change-password and logout endpoints) so that runtime/auth stays free of cell-specific path knowledge (F6 decoupling).
func WithPublicEndpointMatcher ¶
func WithPublicEndpointMatcher(fn func(*http.Request) bool) AuthOption
WithPublicEndpointMatcher sets a compiled method-aware predicate for the auth middleware bypass check.
Router.FinalizeAuth uses this option to install a lazy closure that reads the compiled public-route matcher — aggregated from every Cell's auth.Mount(mux, Route{Public: true}) call — at request time.
ref: otelhttp WithPublicEndpointFn per-request predicate shape
type Authenticator ¶
Authenticator inspects an HTTP request and resolves the caller's identity.
func NewAnonymousAuthenticator ¶
func NewAnonymousAuthenticator() Authenticator
NewAnonymousAuthenticator returns an Authenticator that always succeeds with a fresh PrincipalAnonymous principal. It is the explicit, type-safe way to declare "this WebSocket endpoint accepts unauthenticated traffic" at the composition root — paired with UpgradeConfig.Authenticator's non-nil requirement to keep fail-closed semantics intact.
The returned Principal carries Kind=PrincipalAnonymous and zero ExpiresAt (anonymous principals never expire). Subject and Roles are intentionally empty; downstream authorization (auth.RequireSelfOrRole etc.) treats the anonymous Principal as having no privileges.
func NewBearerHeaderAuthenticator ¶
func NewBearerHeaderAuthenticator(v IntentTokenVerifier) Authenticator
NewBearerHeaderAuthenticator returns an Authenticator that extracts a Bearer token from the Authorization header and delegates verification plus Principal construction to AuthenticateBearer.
Outcomes:
(p, true, nil) — Bearer token present and valid. (absentPrincipal(), false, nil) — no Authorization header, or non-Bearer scheme. (nil, false, err) — Bearer token present but verification rejected it.
This is intentionally a single-scheme authenticator, not a chain/fan-out combinator. Callers that mount it beside other schemes should use separate listener/mount points rather than unioning multiple authenticators.
ref: kubernetes/apiserver pkg/authentication/request/bearertoken/bearertoken.go
func NewContextAuthenticator ¶
func NewContextAuthenticator() Authenticator
NewContextAuthenticator returns an Authenticator that extracts a Principal from the request context. It is used by adapters that mount behind an already-authenticated listener (e.g. WebSocket upgrade routes mounted on a JWT listener); the listener middleware writes the Principal via WithPrincipal, and the adapter's Authenticator simply reads it back.
Outcomes:
(p, true, nil) — Principal found in ctx, Kind != PrincipalUnknown. (absentPrincipal(), false, nil) — no Principal in ctx.
This Authenticator never returns an error. The absent (false, nil) outcome means "the listener middleware did not stamp a Principal"; the consuming adapter MUST treat that as unauthenticated and fail closed (the WebSocket upgrade slot rejects with 401). It is mounted as the sole Authenticator for its endpoint — already-authenticated traffic from a JWT listener is the only expected source.
func NewServiceTokenAuthenticator ¶
func NewServiceTokenAuthenticator(ring kauth.HMACKeyring, clk clock.Clock, opts ...ServiceTokenOption) (Authenticator, error)
NewServiceTokenAuthenticator returns an Authenticator that validates HMAC service tokens (Authorization: ServiceToken <ts>:<nonce>:<mac>).
Returns an error when:
- ring is nil or a typed-nil interface;
- no NonceStore was supplied via WithServiceTokenNonceStore;
- a NoopNonceStore (Kind() == NonceStoreKindNoop) was supplied — replay protection is mandatory at every layer; dev/test wiring must use InMemoryNonceStore (NewInMemoryNonceStore(ServiceTokenNonceTTL, clock.Real())).
This aligns runtime/auth construction with the existing reject-Noop guards in kernel/auth.NewAuthServiceToken, runtime/bootstrap.auth_plan_validate, and cmd/corebundle.SharedDeps.Validate — all four layers fail-closed.
Outcomes (when construction succeeds):
(p, true, nil) — ServiceToken header present and valid. (absentPrincipal(), false, nil) — no Authorization header, or non-"ServiceToken" scheme. (nil, false, err) — ServiceToken present but validation failed (expired, bad MAC, replay).
ref: HashiCorp Vault server fail-closed defaults (no Noop-equivalent path). ref: kubernetes/apiserver pkg/authentication — typed (Authenticator, error).
type AuthenticatorFunc ¶
AuthenticatorFunc is a function that implements Authenticator.
func (AuthenticatorFunc) Authenticate ¶
Authenticate implements Authenticator.
type Authorizer ¶
type Authorizer interface {
// Authorize returns true if the subject is authorized to perform the
// given action on the resource.
Authorize(ctx context.Context, subject, resource, action string) (bool, error)
}
Authorizer checks whether a subject is allowed to perform an action on a resource. Implementations may use RBAC, ABAC, or external policy engines.
type BootstrapAuthFailObserver ¶
BootstrapAuthFailObserver is invoked after a 401 or 429 response is written. The reason string is one of:
- "missing_header" — Basic Auth header absent
- "wrong_credentials" — header present but credentials do not match
- "rate_limited" — per-IP token bucket exhausted (429)
Wiring an observer is how callers route bootstrap auth failures to audit logs without importing cells/ from runtime/auth.
type BootstrapCredentials ¶
BootstrapCredentials carries the env-driven HTTP Basic Auth credentials used to protect the first-admin setup endpoint.
ref: minio/minio internal/auth/credentials.go (length fail-fast at startup) ref: keycloak/keycloak KC_BOOTSTRAP_ADMIN_USERNAME/PASSWORD env model
type BootstrapRateLimiter ¶
BootstrapRateLimiter decides whether a request identified by key should be allowed. Implementations are injected by the caller so that runtime/auth does not import runtime/http/middleware (which imports runtime/auth, creating a cycle). Callers wire a concrete middleware.RateLimiter or an adapters/ratelimit.TokenBucket which satisfies this interface structurally.
There is no built-in "allow all" implementation — bootstrap routes must always carry a real per-IP limiter to defeat brute-force enumeration of operator credentials. Tests construct fakes locally if they need to bypass rate limiting (see runtime/auth/bootstrap_test.go fakeRateLimiter).
type Claims ¶
Claims is a type alias of kauth.Claims so that runtime/auth and kernel/cell share a single canonical struct without conversion at package boundaries. All existing code that references kauth.Claims continues to compile.
type EnvKeyProvider ¶
type EnvKeyProvider struct {
// contains filtered or unexported fields
}
EnvKeyProvider loads key material from environment variables at construction. It succeeds even if some key domains are not configured; individual domain errors surface when RSAKeySet() or HMACKeyRing() is called.
This provider loads keys once (fail-fast per domain). For hot-reload support, see future WM-34 integration.
func NewEnvKeyProvider ¶
func NewEnvKeyProvider(clk clock.Clock, opts ...EnvKeyProviderOption) *EnvKeyProvider
NewEnvKeyProvider loads all available key material from environment variables. It returns a provider even if some key types are not configured — call RSAKeySet() or HMACKeyRing() to check individual domain availability. clk is required; pass clock.Real() at the composition root.
func (*EnvKeyProvider) HMACKeyRing ¶
func (p *EnvKeyProvider) HMACKeyRing() (*HMACKeyRing, error)
HMACKeyRing returns the cached HMACKeyRing or the error from loading.
func (*EnvKeyProvider) RSAKeySet ¶
func (p *EnvKeyProvider) RSAKeySet() (*KeySet, error)
RSAKeySet returns the cached RSA KeySet or the error from loading.
type EnvKeyProviderOption ¶
type EnvKeyProviderOption func(*EnvKeyProvider)
EnvKeyProviderOption configures EnvKeyProvider behavior.
func WithEnvKeyProviderLogger ¶
func WithEnvKeyProviderLogger(l *slog.Logger) EnvKeyProviderOption
WithEnvKeyProviderLogger sets the logger for EnvKeyProvider.
type HMACKeyRing ¶
type HMACKeyRing struct {
// contains filtered or unexported fields
}
HMACKeyRing holds an ordered pair of HMAC secrets for service token operations. Position 0 (current) is used for signing; verification tries all secrets in order.
ref: zeromicro/go-zero rest/token/tokenparser.go — dual-key [current, previous] model ref: gorilla/securecookie — DecodeMulti try-all-keys pattern
func LoadHMACKeyRingFromEnv ¶
func LoadHMACKeyRingFromEnv() (*HMACKeyRing, error)
LoadHMACKeyRingFromEnv loads an HMACKeyRing from environment variables. GOCELL_SERVICE_SECRET is required; GOCELL_SERVICE_SECRET_PREVIOUS is optional.
Both variables are read as raw UTF-8 strings and used directly as HMAC key bytes (no base64 decoding is performed). The value must be at least 32 bytes long. To generate a suitable value: openssl rand -base64 32.
func NewHMACKeyRing ¶
func NewHMACKeyRing(current, previous []byte) (*HMACKeyRing, error)
NewHMACKeyRing creates an HMACKeyRing. current must be at least MinHMACKeyBytes (32 bytes). previous may be nil for single-secret mode; if set, it must also meet the minimum length.
func (*HMACKeyRing) Current ¶
func (r *HMACKeyRing) Current() []byte
Current returns a copy of the active signing secret. The returned slice is a fresh allocation; callers cannot mutate the ring's internal state.
func (*HMACKeyRing) Secrets ¶
func (r *HMACKeyRing) Secrets() [][]byte
Secrets returns a copy of all secrets in try-order: current first, then previous (if set). The returned slice is a fresh allocation; callers cannot mutate the ring's internal state.
type InMemoryNonceOption ¶
type InMemoryNonceOption func(*InMemoryNonceStore)
InMemoryNonceOption configures an InMemoryNonceStore.
func WithMaxNonceEntries ¶
func WithMaxNonceEntries(n int) InMemoryNonceOption
WithMaxNonceEntries overrides the maximum number of live nonce entries before a forced prune is triggered. The default is defaultMaxNonceEntries.
type InMemoryNonceStore ¶
type InMemoryNonceStore struct {
// contains filtered or unexported fields
}
InMemoryNonceStore is a NonceStore backed by a map with lazy expiry pruning. Suitable for single-instance deployments.
func NewInMemoryNonceStore ¶
func NewInMemoryNonceStore(maxAge time.Duration, clk clock.Clock, opts ...InMemoryNonceOption) (*InMemoryNonceStore, error)
NewInMemoryNonceStore creates an InMemoryNonceStore with the given maxAge and clock. clk must not be nil; use clock.Real() for production and clockmock.New() for tests. maxAge must be at least ServiceTokenNonceTTL; a shorter value reintroduces the replay window the store is designed to close, and is rejected with an error.
func (*InMemoryNonceStore) CheckAndMark ¶
func (s *InMemoryNonceStore) CheckAndMark(_ context.Context, nonce string) error
CheckAndMark checks whether nonce has been seen within its TTL window. If not, it records the nonce and returns nil. If the nonce is still live, ErrNonceReused is returned. If the store is at capacity and no entries can be reclaimed, ErrNonceStoreFull is returned.
func (*InMemoryNonceStore) Kind ¶
func (*InMemoryNonceStore) Kind() NonceStoreKind
Kind reports NonceStoreKindInMemory.
func (*InMemoryNonceStore) MaxAge ¶
func (s *InMemoryNonceStore) MaxAge() time.Duration
MaxAge reports the configured nonce retention window. Exposed for startup validation (assert store outlives ServiceTokenMaxAge) and integration tests.
type IntentTokenVerifier ¶
type IntentTokenVerifier = kauth.IntentTokenVerifier
IntentTokenVerifier is a type alias of kauth.IntentTokenVerifier so that runtime/auth and kernel/cell share a single canonical interface without conversion at package boundaries. All existing code that references kauth.IntentTokenVerifier continues to compile without modification.
This is the only verification interface in GoCell. Every production verification path must declare the expected intent to prevent token-confusion attacks (RFC 8725 §3.11).
type IssueOptions ¶
type IssueOptions struct {
Roles []string
Audience []string
SessionID string
PasswordResetRequired bool
// JTI is the JWT ID ("jti" claim). When non-empty it is written into the
// token payload. Empty string omits the claim.
JTI string
// TenantID is the tenant isolation boundary ("tenant_id" claim). When
// non-empty it is written into the token payload so that the verifier can
// extract it into ctxkeys.TenantID for every downstream service call.
// Empty string omits the claim (single-tenant / pre-tenant paths).
// The caller is responsible for passing a canonical UUID string; the issuer
// trusts its caller and does not re-validate.
TenantID string
}
IssueOptions carries optional parameters for JWT issuance. Roles, Audience, and SessionID retain their original semantics. PasswordResetRequired is written as the "password_reset_required" claim only when true; when false the claim is omitted to keep the token compact.
This struct replaces the previous 5-parameter Issue signature (adding PasswordResetRequired would have been the 6th positional argument).
type JWTIssuer ¶
type JWTIssuer struct {
// contains filtered or unexported fields
}
JWTIssuer signs JWT tokens with RS256 using the active key from a SigningKeyProvider. Each issued token carries a kid header derived from the signing key.
func NewJWTIssuer ¶
func NewJWTIssuer(keys SigningKeyProvider, issuer string, ttl time.Duration, clk clock.Clock, opts ...JWTIssuerOption) (*JWTIssuer, error)
NewJWTIssuer creates a JWTIssuer using the active signing key from the provider.
clk is required; pass clock.Real() at the composition root or clockmock.New(...) in tests. Panics on nil or typed-nil clock.
Rejects both plain-nil and typed-nil keys (see NewJWTVerifier).
func (*JWTIssuer) Issue ¶
func (i *JWTIssuer) Issue(intent TokenIntent, subject string, opts IssueOptions) (string, error)
Issue creates a signed JWT token for the given subject and options.
intent declares how the token is meant to be used. GoCell only issues access JWTs. The resulting JWT carries both a JOSE "typ" header (at+jwt) and a "token_use" payload claim so verifiers can reject token-confusion attempts on two independent channels (RFC 9068 §2.1, RFC 8725 §3.11).
The token header includes the kid of the active signing key. When opts.SessionID is non-empty, a "sid" claim binds the token to a specific session for revocation support.
When opts.PasswordResetRequired is true, the claim "password_reset_required" is written into the token payload. When false (the zero value) the claim is omitted entirely for backward compatibility and to minimize token size.
type JWTIssuerOption ¶
type JWTIssuerOption func(*JWTIssuer)
JWTIssuerOption configures a JWTIssuer.
func WithIssuerAudiencesFromSlice ¶
func WithIssuerAudiencesFromSlice(auds []string) JWTIssuerOption
WithIssuerAudiencesFromSlice sets the audience written into tokens when IssueOptions.Audience is nil (fallback / default audience). The slice is copied defensively. Empty or nil slices are silently ignored.
This is the canonical way to configure default audiences when constructing a JWTIssuer from a config.Registry. The config package uses this option internally; production code should prefer config.NewJWTIssuerFromRegistry instead of calling this directly.
type JWTVerifier ¶
type JWTVerifier struct {
// contains filtered or unexported fields
}
JWTVerifier verifies JWT tokens signed with RS256.
ref: go-kratos/kratos middleware/auth/jwt/jwt.go -- JWT middleware pattern Adopted: KeyFunc-based verification, Claims extraction from context. Deviated: RS256 pinned (no configurable signing method), refuses HS256/none. Extended: kid-based key lookup from VerificationKeyStore (RFC 7638 thumbprint).
ref: golang-jwt/jwt v5 parser_option.go -- WithTimeFunc for clock injection. ref: coreos/go-oidc v3 oidc.go IDTokenVerifier -- issuer validation pattern.
func NewJWTVerifier ¶
func NewJWTVerifier(keys VerificationKeyStore, clk clock.Clock, opts ...JWTVerifierOption) (*JWTVerifier, error)
NewJWTVerifier creates a JWTVerifier that validates tokens by looking up the signing key from the VerificationKeyStore using the token's kid header.
clk is required; pass clock.Real() at the composition root or clockmock.New(...) in tests. Panics on nil or typed-nil clock.
Rejects both plain-nil and typed-nil keys (e.g. var ks *KeySet = nil passed through an interface variable): a typed-nil would panic on the first method call downstream, so we fail fast at construction.
func (*JWTVerifier) VerifyIntent ¶
func (v *JWTVerifier) VerifyIntent(ctx context.Context, tokenStr string, expected TokenIntent) (Claims, error)
VerifyIntent validates the token and additionally requires the declared intent (JWT token_use claim + JOSE typ header) to equal expected. Returns ErrAuthInvalidTokenIntent when:
- expected is not a valid TokenIntent
- the token lacks a token_use claim or typ header
- the token's claim/header disagree with each other
- the token's intent does not match expected
This is the primary API for HTTP middleware (expected=access) and the HTTP middleware (expected=access). Refresh endpoints do not verify JWT refresh tokens; they pass opaque wire tokens to refresh.Store.
type JWTVerifierOption ¶
type JWTVerifierOption func(*JWTVerifier)
JWTVerifierOption configures a JWTVerifier.
func WithExpectedAudiences ¶
func WithExpectedAudiences(first string, rest ...string) JWTVerifierOption
WithExpectedAudiences configures VerifyIntent to enforce that the token's aud claim contains at least one of the given audience strings per RFC 8725 §3.3 ("recipients MUST validate the aud claim"). This option is REQUIRED: NewJWTVerifier returns an error if no expected audiences are configured.
The first argument is required (preventing zero-argument calls). Empty strings are silently filtered. Duplicate values across multiple calls are deduplicated.
Verify() is never affected — audience enforcement is intentionally scoped to VerifyIntent only.
ref: RFC 8725 §3.3, RFC 7519 §4.1.3 (aud may be string or array)
func WithExpectedIssuer ¶
func WithExpectedIssuer(iss string) JWTVerifierOption
WithExpectedIssuer configures VerifyIntent to enforce that the token's iss claim exactly matches the given issuer string. The check is applied after audience validation so audience errors remain distinguishable in structured logs. An empty iss argument is silently ignored (no-op), preserving the previous behavior of accepting any issuer.
ref: coreos/go-oidc v3 IDTokenVerifier — issuer validation with equality check ref: golang-jwt/jwt v5 WithIssuer ParserOption — functional option pattern
type KeyProvider ¶
type KeyProvider interface {
// RSAKeySet returns the RSA KeySet for JWT signing and verification.
// Returns an error if no RSA keys are configured.
RSAKeySet() (*KeySet, error)
// HMACKeyRing returns the HMACKeyRing for service token operations.
// Returns an error if no HMAC secrets are configured.
HMACKeyRing() (*HMACKeyRing, error)
}
KeyProvider abstracts the source of cryptographic key material. It is consumed at the composition root (main.go) to build JWTIssuer, JWTVerifier, and service token infrastructure.
Implementations may load from environment variables, files, Vault, or JWKS endpoints. The provider caches loaded keys; consumers call RSAKeySet() or HMACKeyRing() and handle domain-specific errors.
ref: lestrrat-go/jwx — jwk.Set interface for key set abstraction ref: coreos/go-oidc — RemoteKeySet cache-first pattern (future) Adopted: separate RSA / HMAC domains (ISP); cache at construction. Deviated: no remote refresh (FR-017 static config only).
type KeySet ¶
type KeySet struct {
// contains filtered or unexported fields
}
KeySet holds a set of cryptographic keys for JWT operations: one active signing key and zero or more verification-only keys. It provides O(1) key lookup by kid (key identifier). All methods are safe for concurrent use.
ref: dexidp/dex server/rotation.go — 3-state model (Active → Verification-only → Pruned)
ref: 201-wm2-key-rotation — clock injection is needed so rotation tests can advance time without real sleeps; uses kernel/clock.Clock (not func() time.Time) for consistency with the platform clock contract.
func LoadKeySetFromEnv ¶
LoadKeySetFromEnv loads a KeySet from environment variables. It reads the active key pair from GOCELL_JWT_PRIVATE_KEY / GOCELL_JWT_PUBLIC_KEY, and optionally loads a verification-only key from GOCELL_JWT_PREV_PUBLIC_KEY with expiry from GOCELL_JWT_PREV_KEY_EXPIRES (RFC 3339). clk is required; pass clock.Real() at the composition root.
func NewKeySet ¶
func NewKeySet(priv *rsa.PrivateKey, pub *rsa.PublicKey, clk clock.Clock, opts ...KeySetOption) (*KeySet, error)
NewKeySet creates a KeySet with a single active signing key pair. The kid is derived deterministically from the public key using RFC 7638.
clk is required; pass clock.Real() at the composition root or clockmock.New(...) in tests. Panics on nil or typed-nil clock.
func NewKeySetWithVerificationKeys ¶
func NewKeySetWithVerificationKeys( priv *rsa.PrivateKey, pub *rsa.PublicKey, clk clock.Clock, vkeys []VerificationKey, opts ...KeySetOption, ) (*KeySet, error)
NewKeySetWithVerificationKeys creates a KeySet with an active signing key and one or more verification-only keys. Keys that are already expired at construction time are pruned immediately.
func (*KeySet) PruneExpired ¶
func (ks *KeySet) PruneExpired()
PruneExpired removes verification-only keys whose expiry time has passed from the internal maps. This is an explicit cleanup operation — call it at rotation boundaries or during maintenance, not on the request path. Safe for concurrent use.
func (*KeySet) PublicKeyByKID ¶
PublicKeyByKID looks up a public key by its kid. For verification-only keys, it checks the expiry time and rejects expired keys. This method is a pure-read operation — it does not mutate internal state. Safe for concurrent use.
ref: dexidp/dex, hashicorp/vault, gravitational/teleport — all keep the verification/lookup path read-only; lifecycle mutations happen at the rotation/loading boundary, not on every request.
func (*KeySet) SigningKey ¶
func (ks *KeySet) SigningKey() *rsa.PrivateKey
SigningKey returns the active private key for signing. The returned pointer is shared — the caller must not modify the key or its Precomputed fields. Unlike HMACKeyRing.Current(), RSA private keys are returned by reference because deep-copying is expensive and unnecessary for the signing hot path.
func (*KeySet) SigningKeyID ¶
SigningKeyID returns the kid of the active signing key.
type KeySetOption ¶
type KeySetOption func(*KeySet)
KeySetOption configures KeySet behavior.
func WithKeySetLogger ¶
func WithKeySetLogger(l *slog.Logger) KeySetOption
WithKeySetLogger sets the logger for a KeySet.
type NonceStore ¶
type NonceStore interface {
CheckAndMark(ctx context.Context, nonce string) error
Kind() NonceStoreKind
}
NonceStore tracks nonces for replay prevention. Implementations must be safe for concurrent use. The store must retain nonces for at least ServiceTokenNonceTTL to prevent replay across the token validity window plus the accepted future clock skew; a shorter TTL creates a replay vulnerability.
Kind reports the implementation classification for startup validation. Control-plane guards in adapter mode "real" must reject NonceStoreKindNoop so replay protection is never silently disabled in production.
This interface is identical to kauth.NonceStore; runtime/auth types that implement it automatically satisfy the kernel interface.
type NonceStoreKind ¶
type NonceStoreKind = kauth.NonceStoreKind
NonceStoreKind classifies a NonceStore implementation for startup validation. It is an alias of kauth.NonceStoreKind so that runtime/auth types satisfy the kernel interface directly without conversion.
type NoopNonceStore ¶
type NoopNonceStore struct{}
NoopNonceStore is an explicit null-object implementation of NonceStore that always permits a nonce. It exists so that callers never carry a nil NonceStore through the authenticator pipeline — every code path operates on a non-nil implementation, and dev-mode opt-out is explicit rather than accidental.
cmd/corebundle.SharedDeps.Validate rejects this implementation in adapter mode "real" (see errcode.ErrControlplaneNonceStoreMissing).
func NewNoopNonceStore ¶
func NewNoopNonceStore() NoopNonceStore
NewNoopNonceStore returns the sentinel NoopNonceStore value.
func (NoopNonceStore) CheckAndMark ¶
func (NoopNonceStore) CheckAndMark(context.Context, string) error
CheckAndMark always returns nil — replay detection is disabled.
func (NoopNonceStore) Kind ¶
func (NoopNonceStore) Kind() NonceStoreKind
Kind reports NonceStoreKindNoop.
type Policy ¶
Policy evaluates an HTTP request for authorization. A nil return permits the request; a non-nil errcode error short-circuits with the mapped status. Taking *http.Request (not just context) lets policies read path/query params for self-access checks without handler cooperation.
ref: go-chi/jwtauth Authenticator — middleware wraps handler; error-only decision. Deviated: Policy takes *http.Request instead of context so that SelfOr can read path values directly (r.PathValue) without the handler passing the target ID in explicitly. ref: grpc-ecosystem/go-grpc-middleware WithServerUnaryInterceptor — interceptor pattern where auth is declared at registration time, not inside handler body.
func AnyRole ¶
AnyRole returns a Policy that requires the subject to hold at least one of the given roles. Wraps RequireAnyRole.
Footgun: calling AnyRole() with zero roles produces a Policy that always returns ErrAuthForbidden (no role can match). Pass at least one named role, or use Public:true on the auth.Route if the endpoint should be unauthenticated.
func RequireCallerCell ¶
RequireCallerCell returns a Policy that enforces the request is made by a service principal (PrincipalService) whose CallerCellID is in the allowlist.
Use in auth.Route.Policy for internal endpoints that declare Clients in their ContractSpec; auth.Mount auto-applies this guard when spec.Clients is non-empty.
Errors:
- ErrAuthUnauthorized: no Principal in context
- ErrAuthForbidden: Principal is not PrincipalService, or CallerCellID is empty, or CallerCellID not in allowlist
func SelfOr ¶
SelfOr returns a Policy that permits the request when the subject equals the path parameter value, or when the subject holds one of bypassRoles. pathParam is the name of the path parameter (e.g. "id", "userID") whose value is compared against the authenticated subject via r.PathValue.
If the path value matches the strict canonical UUID form (36-char dashed or 32-char compact, any case), it is normalized to lowercase canonical dashed form before comparison. Brace-wrapped, urn:uuid:, and whitespace-padded shapes are NOT treated as UUIDs here even though google/uuid.Parse accepts them — they fall through to verbatim comparison and almost always fail to match a canonical subject, leaving the bypassRoles check as the only path to authorization. Non-UUID path values (e.g. roleName) are compared verbatim.
If the path parameter is empty (route does not carry it), the check falls back to role-only; prefer AnyRole for those endpoints.
type Principal ¶
type Principal struct {
Kind PrincipalKind
Subject string
Roles []string
AuthMethod string
PasswordResetRequired bool
// CallerCellID is the originating cell id for service principals. It is
// extracted from the 4-part service token (ts:nonce:callerCell:mac) and
// checked against ContractSpec.Clients by RequireCallerCell. Empty for
// user and anonymous principals.
CallerCellID string
// TenantID is the tenant isolation boundary the request belongs to, sourced
// from the JWT "tenant_id" claim. JWTVerifier.VerifyIntent (the single
// unbypassable chokepoint for JWT→Claims) has already validated and
// canonicalized it via pkg/tenant.ParseTenantID (a malformed claim is
// rejected before any Principal is built), so a non-empty value is a
// canonical lowercase UUID. Empty for service principals (a service token's
// callerCell is NOT a tenant), anonymous principals, and single-tenant
// deployments. injectPrincipalCtxKeys propagates a non-empty value to
// ctxkeys.TenantID for the outbox principal envelope.
//
// Deliberately typed string, not pkg/tenant.TenantID: the ctx/wire principal
// bridge is type-uniform string/SafeID (ctxkeys.With{Actor,Subject,Tenant,
// Session}ID all take string; outbox.PrincipalMetadata.TenantID is a frozen
// idutil.SafeID). tenant.TenantID is the REPO-layer typed parameter (PR-2's
// "漏传=compile error" Hard funnel); threading it through the wire path would
// only add string↔TenantID↔SafeID conversions. The repo layer re-types this
// string via tenant.ParseTenantID at its boundary.
TenantID string
// Claims is a read-only snapshot of supplementary JWT claims (e.g. "sid",
// "iss", "token_use"). Callers must treat Claims as a read-only snapshot;
// mutating it has no effect on authentication decisions and may corrupt
// shared state.
Claims map[string]string
// ExpiresAt is the absolute expiration time of the credential that
// produced this Principal. Zero value means "no expiry" (anonymous
// and service principals never expire). For JWT principals this is
// copied from Claims.ExpiresAt by jwtClaimsToPrincipal.
//
// Consumers (e.g. runtime/websocket.Hub) check ExpiresAt against the
// current clock to decide eviction; long-lived WebSocket connections
// are evicted on the next ping tick after token expiry.
ExpiresAt time.Time
}
Principal is the unified authn subject injected into request context after successful authentication. AuthMiddleware and ServiceTokenMiddleware both populate Principal via WithPrincipal; handlers consume it via FromContext. Principal is the authoritative authn context for all business routes (F1/F7).
For service principals (Kind==PrincipalService): identity is expressed via CallerCellID (the originating cell id extracted from the 4-part service token). Subject is always empty; Roles is always nil. Use RequireCallerCell to authorize internal endpoints by caller allowlist.
Immutability contract: After an Authenticator returns a Principal, callers must not modify any field for the lifetime of the request/connection. runtime/websocket.Hub snapshots Subject and ExpiresAt at handshake time and treats the principal as immutable; mutating Subject/Roles/ExpiresAt/Claims after Authenticate has undefined effects on hub subjectIdx and expiry eviction. Slices (Roles) and maps (Claims) are read-only by convention; do not append or mutate. This mirrors the existing Claims map convention.
func AuthenticateBearer ¶
func AuthenticateBearer(ctx context.Context, verifier IntentTokenVerifier, token string) (context.Context, *Principal, error)
AuthenticateBearer verifies the bearer token (intent=access) and, on success, returns a context enriched with the verified *Principal: both WithPrincipal and the principal ctxkeys (actor/subject/session) that carry identity across the async outbox boundary. It is the single transport-agnostic authentication core shared by the HTTP AuthMiddleware (handleAuthRequest) and the gRPC unary auth interceptor (runtime/grpc/interceptor) so neither transport duplicates the verify→principal→ctx bridge.
SECURITY: there is deliberately NO claims-input variant. A *Principal can only be produced from a token that the verifier accepts — no caller (cell, interceptor, …) can forge a principal by fabricating Claims. The principal ctxkey writes stay physically inside this file, so the CTXKEYS-PRINCIPAL-WRITE-CALLER-01 funnel is intact; exporting this function does not widen the trust boundary because it gates on VerifyIntent.
On verify failure the input ctx is returned unchanged together with the raw verifier error; callers classify it (errcode.IsExpected4xx / KindUnavailable) to render their transport-specific rejection.
G1.A: tokens with an empty "sub" claim are rejected before any principal or ctxkeys are injected. An empty subject indicates a JWT signing bug or OIDC misconfiguration; accepting it would allow a bearer with roles to pass RequireAnyRole unchecked (aligned with authenticator.go G1.A).
type PrincipalKind ¶
type PrincipalKind int
const ( // PrincipalUnknown is the zero value of PrincipalKind; it indicates an // uninitialised Principal and must never appear in a fully-constructed // Principal returned by an Authenticator. PrincipalUnknown PrincipalKind = iota PrincipalUser // JWT user PrincipalService // service token / mTLS machine PrincipalAnonymous // public endpoint // PrincipalDevice is a first-class device subject (e.g. an MDM-enrolled // device), distinct from a human user — it feeds device-posture attributes // to downstream ABAC / zero-trust evaluation. No device-token issuer exists // on develop yet (the JWT authenticator still classifies every verified // token as PrincipalUser); the kind is the type-foundation half of the // multi-tenancy/ABAC epic. Adding it forces every PrincipalKind switch to // gain an explicit branch — guarded by archtest // PRINCIPAL-KIND-EXHAUSTIVE-SWITCH-01. PrincipalDevice )
func (PrincipalKind) String ¶
func (k PrincipalKind) String() string
type Route ¶
type Route struct {
// Contract is the ContractSpec to bind to this route. REQUIRED. Its
// Method + Path drive both the chi registration pattern and the span
// attributes (gocell.contract.id / kind / transport + http.method /
// route). Contract.Kind MUST be "http".
Contract contractspec.ContractSpec
// Handler is the inner http.Handler. REQUIRED.
Handler http.Handler
// Policy is the per-route authorization policy enforced by
// RequirePolicy. Nil = no guard.
Policy Policy
// Public marks a JWT-exempt route. Public routes MUST carry no Policy
// (server-side authorization has no subject to authorize) and no
// PasswordResetExempt (the gate runs only for authenticated tokens).
Public bool
// PasswordResetExempt allows reset-required tokens through this route.
PasswordResetExempt bool
// BootstrapAuth, when non-nil, marks the route as protected by per-route
// replacement authentication (HTTP Basic Auth using
// GOCELL_BOOTSTRAP_ADMIN_USERNAME/PASSWORD env credentials, supplied by
// the composition root via runtime/auth.NewBootstrapMiddleware). The
// listener-level JWT middleware skips this route (via FinalizeAuth
// matcher); the function passed here authenticates instead. The non-nil
// presence of this field is the single source of truth — there is no
// separate Bootstrap bool flag.
//
// BootstrapAuth is mutually exclusive with Public, PasswordResetExempt,
// and Policy: a route either runs through the listener auth chain (with
// optional Public / PasswordResetExempt overrides + Policy guard) or it
// runs through the bootstrap middleware. Combining the two would mean
// "bypass JWT but also enforce JWT-time policies" — a contradiction that
// validateBypassCompatibility rejects at registration time.
//
// ref: docs/architecture/202605061600-adr-bootstrap-admin-boundary.md §D1
BootstrapAuth func(http.Handler) http.Handler
// IdempotencyExempt, when true, instructs the HTTP idempotency middleware
// to never claim or record this route's responses — for endpoints whose
// responses must not be persisted or replayed (e.g. credential rotation,
// change-password). The route still executes normally; clients sending an
// Idempotency-Key header receive no replay guarantee for this route.
//
// IdempotencyExempt is orthogonal to Public, PasswordResetExempt, and
// BootstrapAuth — any combination is allowed.
IdempotencyExempt bool
}
Route binds a handler to a contract. Contract is the single source of truth for HTTP method + fully-qualified path + observability metadata — runtime-side Method/Path fields have been eliminated in round-4 (the pre-round-4 dual-maintenance of Route.Method/Path vs Contract.Method/Path was the drift surface reviewers called out; the collapse pins the two to the same literal).
Mount derives the chi sub-route-relative registration path by stripping the receiving mux's Prefix() (when the mux implements cell.Prefixer; runtime/http/router.Router's nested chiRouterAdapter does). Stdlib *http.ServeMux and test stubs without a prefix use Contract.Path unchanged.
type ServiceTokenOption ¶
type ServiceTokenOption func(*serviceTokenConfig)
ServiceTokenOption configures ServiceTokenMiddleware behavior.
func WithServiceTokenLogger ¶
func WithServiceTokenLogger(l *slog.Logger) ServiceTokenOption
WithServiceTokenLogger sets the logger for ServiceTokenMiddleware.
func WithServiceTokenMetrics ¶
func WithServiceTokenMetrics(m *AuthMetrics) ServiceTokenOption
WithServiceTokenMetrics sets the AuthMetrics for ServiceTokenMiddleware.
func WithServiceTokenNonceStore ¶
func WithServiceTokenNonceStore(ns NonceStore) ServiceTokenOption
WithServiceTokenNonceStore configures the replay-defense store. The middleware rejects nonces already consumed within the store's TTL window. Replay protection is mandatory — both ServiceTokenMiddleware and NewServiceTokenAuthenticator reject nil/Noop NonceStore at construction time. Use NewInMemoryNonceStore(ServiceTokenNonceTTL, clock.Real()) for dev/test wiring.
Passing nil is a no-op: cfg.nonceStore stays nil and construction will fail.
type SigningKeyProvider ¶
type SigningKeyProvider interface {
// SigningKey returns the active RSA private key for signing tokens.
SigningKey() *rsa.PrivateKey
// SigningKeyID returns the kid (key identifier) of the active signing key.
SigningKeyID() string
}
SigningKeyProvider supplies the active signing key for JWT issuance. Implementations must be safe for concurrent use.
*KeySet satisfies this interface.
type StaticKeyProvider ¶
type StaticKeyProvider struct {
// contains filtered or unexported fields
}
StaticKeyProvider holds pre-constructed key material. Useful for tests and composition roots that load keys before building the provider.
func NewStaticKeyProvider ¶
func NewStaticKeyProvider(ks *KeySet, ring *HMACKeyRing) *StaticKeyProvider
NewStaticKeyProvider creates a KeyProvider from pre-existing key material. Either parameter may be nil; the corresponding getter returns an error.
func (*StaticKeyProvider) HMACKeyRing ¶
func (p *StaticKeyProvider) HMACKeyRing() (*HMACKeyRing, error)
HMACKeyRing returns the pre-loaded HMACKeyRing or an error if nil.
func (*StaticKeyProvider) RSAKeySet ¶
func (p *StaticKeyProvider) RSAKeySet() (*KeySet, error)
RSAKeySet returns the pre-loaded KeySet or an error if nil.
type TokenIntent ¶
type TokenIntent = kauth.TokenIntent
TokenIntent is a type alias of kauth.TokenIntent so that runtime/auth and kernel/cell share a single canonical type without conversion at package boundaries. All existing code that references kauth.TokenIntent continues to compile without modification.
ref: RFC 9068 §2.1 (typ: at+jwt), RFC 8725 §3.11 (token confusion defense) ref: AWS Cognito token_use claim ("access"/"id"), Keycloak TokenUtil.java
type VerificationKey ¶
VerificationKey is a previously active signing key that has been demoted. It retains the public key for token validation during the grace period.
type VerificationKeyStore ¶
type VerificationKeyStore interface {
// PublicKeyByKID returns the public key matching the given kid.
// Returns an error for unknown or expired kids.
PublicKeyByKID(kid string) (*rsa.PublicKey, error)
}
VerificationKeyStore looks up public keys for JWT verification by kid. Implementations must be safe for concurrent use.
*KeySet satisfies this interface.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package config is the production wiring entrypoint for runtime/auth components.
|
Package config is the production wiring entrypoint for runtime/auth components. |
|
Package credentialfence is the sealed source of the credential-invalidation FenceToken — a capability proof that authorizes calls to the three credential mutation methods (session.Store.RevokeForSubject / refresh.Store.RevokeUser / ports.UserRepository.BumpAuthzEpoch).
|
Package credentialfence is the sealed source of the credential-invalidation FenceToken — a capability proof that authorizes calls to the three credential mutation methods (session.Store.RevokeForSubject / refresh.Store.RevokeUser / ports.UserRepository.BumpAuthzEpoch). |
|
Package keystest provides RSA / HMAC key fixtures for tests that exercise the auth subsystem (JWT issue/verify, HMAC service-token rings, OIDC stubs).
|
Package keystest provides RSA / HMAC key fixtures for tests that exercise the auth subsystem (JWT issue/verify, HMAC service-token rings, OIDC stubs). |
|
This file declares the two public error sentinels for refresh.Store.
|
This file declares the two public error sentinels for refresh.Store. |
|
memstore
Package memstore provides an in-memory implementation of refresh.Store.
|
Package memstore provides an in-memory implementation of refresh.Store. |
|
storetest
Package storetest provides a reusable contract test suite for refresh.Store implementations.
|
Package storetest provides a reusable contract test suite for refresh.Store implementations. |
|
Package session declares the typed-Go-heavy Protocol primitive that bundles session-related protocol decisions for accesscore (and any future cell that owns server-side session state).
|
Package session declares the typed-Go-heavy Protocol primitive that bundles session-related protocol decisions for accesscore (and any future cell that owns server-side session state). |
|
sessiontest
Package sessiontest provides test helpers for code that depends on runtime/auth/session.
|
Package sessiontest provides test helpers for code that depends on runtime/auth/session. |
|
storetest
Package storetest provides a reusable Protocol-driven contract test suite for session.Store implementations.
|
Package storetest provides a reusable Protocol-driven contract test suite for session.Store implementations. |