Documentation
¶
Overview ¶
Package auth provides JWT and API-key authentication for forge-generated Connect RPC services.
The library exposes a Validator that authenticates incoming Connect requests against a configured provider ("jwt", "api_key", "both", or "none") and a Connect interceptor that attaches the resulting Claims to the request context.
Generated forge projects import this package via a thin shim in pkg/middleware/auth_gen.go. The shim wires the project's Config from forge.yaml and exposes a project-local Claims alias (type Claims = auth.Claims).
Index ¶
- Constants
- func JWKSKeyfunc(urls ...string) (jwt.Keyfunc, error)
- type APIKeyConfig
- type Claims
- type ClerkOpts
- type Config
- type FirebaseOpts
- type HMACValidator
- type HMACValidatorConfig
- type InterceptorOptions
- type JWKSValidator
- type JWKSValidatorConfig
- type JWTConfig
- type KeyValidator
- type MultiValidator
- type TokenValidator
- type UserResolver
- type Validator
- func (v *Validator) AuthenticateHeaders(ctx context.Context, headers http.Header, opts InterceptorOptions) (*Claims, error)
- func (v *Validator) Close() error
- func (v *Validator) ConnectInterceptor(opts InterceptorOptions, ...) connect.Interceptor
- func (v *Validator) Interceptor(opts InterceptorOptions, ...) connect.UnaryInterceptorFunc
- func (v *Validator) IsUnauthenticatedProcedure(procedure string, skipMethods []string) bool
- func (v *Validator) Provider() string
- func (v *Validator) SetKeyValidator(kv KeyValidator)
- func (v *Validator) Validate(token string) (*Claims, error)
Constants ¶
const ( ProviderNone = "none" ProviderJWT = "jwt" ProviderAPIKey = "api_key" ProviderBoth = "both" )
Provider names recognized by Config.
Variables ¶
This section is empty.
Functions ¶
func JWKSKeyfunc ¶
JWKSKeyfunc builds an auto-refreshing jwt.Keyfunc backed by one or more JWKS endpoints. Signing keys are fetched once at construction (so a misconfigured or unreachable endpoint fails fast, at server boot) and then refreshed in the background for the life of the process.
The fail-fast half is what NoErrorReturnFirstHTTPReq buys: keyfunc's NewDefault DEFAULTS to swallowing the first fetch error and logging it, which turns a typo'd JWKS URL into an endless stream of per-request 401s nobody can attribute. A server that cannot reach its issuer's keys cannot authenticate anyone, so refusing to boot is both louder and no less available.
It is the seam Clerk and Firebase use internally; call it directly when wiring a custom JWKS issuer through NewJWKSValidator.
Types ¶
type APIKeyConfig ¶
type APIKeyConfig struct {
// Header is the HTTP header name carrying the API key.
// Defaults to "X-API-Key" when empty.
Header string
}
APIKeyConfig holds API-key-specific settings.
func (APIKeyConfig) EffectiveHeader ¶
func (a APIKeyConfig) EffectiveHeader() string
EffectiveHeader returns Header or the "X-API-Key" default.
type Claims ¶
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
OrgID string `json:"org_id"`
Role string `json:"role"`
Roles []string `json:"roles"`
Raw map[string]any `json:"raw,omitempty"`
}
Claims is the canonical claims shape produced by all forge auth flows.
The struct mirrors the per-project pkg/middleware/Claims that earlier forge versions generated. The generated shim re-exports it via type Claims = auth.Claims so existing project code (referring to middleware.Claims) keeps compiling.
Raw carries the full decoded JWT payload (or any provider-specific map a UserResolver chooses to attach). Projects that need access to claims outside the fixed fields — Supabase user_metadata, Auth0 app_metadata, Clerk org_permissions, etc. — should read from Raw instead of forking the Claims type.
type ClerkOpts ¶
type ClerkOpts struct {
// JWKSURL is the Clerk instance's JWKS endpoint, e.g.
// https://<your-subdomain>.clerk.accounts.dev/.well-known/jwks.json
// (typically read from CLERK_JWKS_URL). Required unless KeyFunc is set.
JWKSURL string
// Issuer, when set, is enforced (iss). Clerk's issuer is the instance's
// Frontend API URL, e.g. https://<your-subdomain>.clerk.accounts.dev.
// Strongly recommended in production so a token minted for a different
// Clerk instance is rejected.
Issuer string
// Audience, when set, is enforced (aud).
Audience string
// KeyFunc overrides JWKS fetching. Set it to inject a pre-built keyfunc
// (tests, or a custom JWKS client); when nil the validator builds one
// from JWKSURL via [JWKSKeyfunc].
KeyFunc jwt.Keyfunc
}
ClerkOpts configures a Clerk validator.
type Config ¶
type Config struct {
// Provider is one of "jwt", "api_key", "both", "none". Required.
Provider string
// JWT configures JWT validation. Used when Provider is "jwt" or "both".
JWT JWTConfig
// APIKey configures API key validation. Used when Provider is
// "api_key" or "both". A [KeyValidator] must be supplied separately
// via [Validator.SetKeyValidator] or [Config.KeyValidator].
APIKey APIKeyConfig
// SkipMethods is the list of fully-qualified Connect procedure names
// that bypass auth (in addition to the built-in gRPC health-check skip).
SkipMethods []string
// KeyValidator validates API keys. Required when APIKey auth is enabled.
KeyValidator KeyValidator
// TokenValidators, when non-empty, replaces the built-in single-secret
// JWT path with an ordered fallback chain. Each entry validates the
// bearer token independently; the first to accept wins. Use this when
// a service must accept tokens from more than one issuer — typically
// during an auth-provider migration (e.g. Supabase HMAC tokens
// alongside Auth0 JWKS tokens).
//
// When empty the legacy JWT (single secret / JWKSURL) path is used,
// preserving backwards compatibility for projects that only need one
// validator.
TokenValidators []TokenValidator
// UserResolver, when non-nil, projects the raw decoded JWT payload onto
// [Claims]. Use it when provider-specific claim shapes (Supabase
// user_metadata, Auth0 app_metadata, Clerk org_permissions, etc.) need
// to drive the Claims you hand to downstream code.
//
// When nil the validator uses built-in shape extraction (sub, email,
// org_id, role, roles) and still attaches the full payload to
// Claims.Raw so downstream code can inspect it.
UserResolver UserResolver
}
Config configures a Validator.
Provider selects the authentication scheme. JWT, APIKey and SkipMethods further customize each scheme. Every value is supplied by the caller — this package reads no environment variable of its own, so the key a validator trusts is always the key its constructor was handed.
type FirebaseOpts ¶
type FirebaseOpts struct {
// ProjectIDs is the set of accepted Firebase project IDs (typically read
// from FIREBASE_PROJECT_IDS, comma-separated). A token is accepted only
// when its aud is one of these AND its iss is
// securetoken.google.com/<that same project>. At least one is required.
// Accepting several lets one binary serve clients from multiple Firebase
// projects.
ProjectIDs []string
// KeyFunc overrides Google's JWKS endpoint (tests, or a custom key
// source). When nil the validator builds one from the well-known
// Firebase JWKS URL via [JWKSKeyfunc].
KeyFunc jwt.Keyfunc
}
FirebaseOpts configures a Firebase validator.
type HMACValidator ¶
type HMACValidator struct {
// contains filtered or unexported fields
}
HMACValidator validates JWTs signed with an HMAC algorithm (HS256/384/512).
Zero value is not usable; construct with NewHMACValidator. The secret is captured at construction; rotating it means constructing a new validator (which is cheap — no I/O).
func NewHMACValidator ¶
func NewHMACValidator(cfg HMACValidatorConfig) (*HMACValidator, error)
NewHMACValidator constructs a validator for HMAC-signed JWTs.
Returns an error when Secret is empty or SigningMethod is not an HS* algorithm.
func (*HMACValidator) ValidateToken ¶
func (v *HMACValidator) ValidateToken(tokenString string) (*Claims, error)
ValidateToken implements TokenValidator.
type HMACValidatorConfig ¶
type HMACValidatorConfig struct {
// SigningMethod is the expected JWT alg (e.g. "HS256"). Required.
SigningMethod string
// Secret is the symmetric secret. Required — this package reads no
// environment variable of its own, so the key a validator trusts is
// always the key its constructor was handed. Pass it from the app's
// typed config (declare the field `sensitive: true` and it arrives from
// the environment's secret provider, never as an inline manifest
// value); an app accepting several issuers passes each their own.
Secret string
// Issuer, when set, is enforced via jwt.WithIssuer.
Issuer string
// Audience, when set, is enforced via jwt.WithAudience.
Audience string
// Resolver, when non-nil, is given the raw JWT payload so projects can
// project provider-specific shapes onto [Claims] (and into Claims.Raw).
// Nil means use the built-in shape extraction.
Resolver UserResolver
}
HMACValidatorConfig configures an HMACValidator.
type InterceptorOptions ¶
type InterceptorOptions struct {
// SkipMethods overrides Config.SkipMethods when non-nil.
SkipMethods []string
}
InterceptorOptions configures Validator.Interceptor behaviour beyond the fields already on Config.
type JWKSValidator ¶
type JWKSValidator struct {
// contains filtered or unexported fields
}
JWKSValidator validates JWTs whose signing key comes from a JWKS endpoint (or any other jwt.Keyfunc source — the pkg/auth library deliberately does not depend on a JWKS client implementation; the caller supplies one).
Construct via NewJWKSValidator with a pre-built jwt.Keyfunc (e.g. one returned by github.com/MicahParks/keyfunc/v3.NewDefaultCtx).
func NewJWKSValidator ¶
func NewJWKSValidator(cfg JWKSValidatorConfig) (*JWKSValidator, error)
NewJWKSValidator constructs a validator that delegates key resolution to cfg.KeyFunc (typically a JWKS-backed implementation).
func (*JWKSValidator) ValidateToken ¶
func (v *JWKSValidator) ValidateToken(tokenString string) (*Claims, error)
ValidateToken implements TokenValidator.
type JWKSValidatorConfig ¶
type JWKSValidatorConfig struct {
// SigningMethod is the expected JWT alg. Defaults to "RS256".
SigningMethod string
// KeyFunc resolves the signing key for the token. Required.
KeyFunc jwt.Keyfunc
// Issuer, when set, is enforced via jwt.WithIssuer.
Issuer string
// Audience, when set, is enforced via jwt.WithAudience.
Audience string
// Resolver, when non-nil, projects the raw JWT payload onto [Claims].
Resolver UserResolver
}
JWKSValidatorConfig configures a JWKSValidator.
type JWTConfig ¶
type JWTConfig struct {
// SigningMethod is the expected JWT alg value (e.g. "HS256", "RS256").
// Defaults to "RS256" when empty.
SigningMethod string
// Issuer, when set, is enforced via jwt.WithIssuer.
Issuer string
// Audience, when set, is enforced via jwt.WithAudience.
Audience string
// JWKSURL, when set, is the issuer's JWKS endpoint. [NewValidator]
// fetches it ONCE at construction (so an unreachable or malformed
// endpoint fails at server boot, not on the first request) and refreshes
// it in the background for the life of the process — the same
// [JWKSKeyfunc] machinery [Clerk] and [Firebase] use. This is the path
// every standard OIDC issuer (Auth0, Keycloak, Zitadel, Supabase, Okta)
// takes.
//
// Mutually exclusive with Secret: two key sources for one validator is a
// configuration error, and [NewValidator] rejects it rather than picking
// a winner. A service that must genuinely accept tokens from more than
// one issuer composes them explicitly via [Config.TokenValidators].
JWKSURL string
// Secret is the symmetric secret (HS*) or PEM-encoded public key
// (RS*/ES*). Mutually exclusive with JWKSURL.
Secret string
// KeyFunc, when non-nil, fully overrides key resolution. Useful for tests
// and for callers wiring custom JWKS clients.
KeyFunc jwt.Keyfunc
}
JWTConfig holds JWT-specific settings.
func (JWTConfig) EffectiveSigningMethod ¶
EffectiveSigningMethod returns SigningMethod or the "RS256" default.
type KeyValidator ¶
KeyValidator validates an API key and returns the associated claims. Implementations typically look up the key in a database or cache.
type MultiValidator ¶
type MultiValidator struct {
// contains filtered or unexported fields
}
MultiValidator tries each underlying validator in order and returns the first set of claims that parses cleanly.
When every validator rejects the token, MultiValidator returns the LAST validator's error (the chain typically ends with the strictest validator, so its error is the most informative). Use NewMultiValidator to construct.
func NewMultiValidator ¶
func NewMultiValidator(validators ...TokenValidator) (*MultiValidator, error)
NewMultiValidator constructs an ordered fallback chain. At least one underlying validator is required.
func (*MultiValidator) ValidateToken ¶
func (m *MultiValidator) ValidateToken(tokenString string) (*Claims, error)
ValidateToken implements TokenValidator by trying each underlying validator in order.
type TokenValidator ¶
type TokenValidator interface {
// ValidateToken parses tokenString and returns the parsed Claims. The
// returned error must be non-nil iff the token is invalid for THIS
// validator. MultiValidator uses error to decide whether to try the
// next validator in the chain.
ValidateToken(tokenString string) (*Claims, error)
}
TokenValidator validates a single bearer JWT and returns parsed Claims.
Implementations are expected to be safe for concurrent use. Each Validator represents one issuer/key configuration (e.g. one HMAC secret, one JWKS endpoint); compose multiple via MultiValidator when a service needs to accept tokens from more than one issuer (typical during an auth-provider migration — Supabase HMAC tokens alongside Auth0 JWKS tokens).
type UserResolver ¶
type UserResolver interface {
// Resolve receives the full decoded JWT payload (as a map, the
// shape jwt-go hands us) and must return a populated [Claims]. The
// raw payload should typically be copied onto Claims.Raw so
// downstream callers can still inspect provider-specific fields.
Resolve(rawClaims map[string]any) (*Claims, error)
}
UserResolver is an optional hook that translates a raw decoded JWT payload into Claims. Projects implement it when they need to consume provider-specific claim shapes (e.g. Supabase user_metadata, Auth0 app_metadata, Clerk org_role/org_permissions) without forking the Claims type.
The resolver is wired via Config.UserResolver; when nil the validator falls back to the built-in shape extraction (sub, email, org_id, role, roles) and still populates Claims.Raw with the full payload.
type Validator ¶
type Validator struct {
// contains filtered or unexported fields
}
Validator authenticates Connect RPC requests against a configured provider. Construct with NewValidator; reuse a single Validator per service.
func Clerk ¶
Clerk returns a Validator for Clerk session tokens. Clerk issues RS256 JWTs whose signing keys come from a per-instance JWKS endpoint; the claim shape (sub / org_id / org_role / org_permissions) is mapped onto Claims by [clerkResolver].
// in SetupAuth(cfg *config.Config) — the values are typed config
// fields, declared in proto/config/v1/config.proto and pinned per
// environment, never read out of the process environment here.
validator, err := auth.Clerk(auth.ClerkOpts{
JWKSURL: cfg.JwtJwksUrl,
Issuer: cfg.JwtIssuer, // optional but recommended
})
return validator.Validate, nil
func Firebase ¶
func Firebase(opts FirebaseOpts) (*Validator, error)
Firebase returns a Validator for Firebase ID tokens. Firebase issues RS256 JWTs signed with Google's rotating keys; each accepted project pins its own iss+aud, so per-project enforcement is expressed as one JWKSValidator per project composed through a MultiValidator. Claims are mapped by [firebaseResolver] (sub = uid).
// A value this app's config does not carry yet gets a new field in
// proto/config/v1/config.proto — that is the one place a
// per-deployment value is declared.
validator, err := auth.Firebase(auth.FirebaseOpts{
ProjectIDs: strings.Split(cfg.FirebaseProjectIds, ","),
})
func NewValidator ¶
NewValidator returns a Validator wired for cfg.Provider.
It returns an error when Provider is unrecognized. The "none" provider is allowed and produces a Validator whose Interceptor is a no-op (useful for tests and for projects that haven't enabled auth yet).
func (*Validator) AuthenticateHeaders ¶
func (v *Validator) AuthenticateHeaders(ctx context.Context, headers http.Header, opts InterceptorOptions) (*Claims, error)
AuthenticateHeaders authenticates a request given its headers and returns the parsed claims (or an error). It is the testable core of Validator.Interceptor — the interceptor wraps this with the procedure-skip logic and context plumbing.
func (*Validator) Close ¶
Close releases any resources held by the Validator. Currently a no-op; kept for forward compatibility with JWKS-cache implementations.
func (*Validator) ConnectInterceptor ¶
func (v *Validator) ConnectInterceptor(opts InterceptorOptions, withClaims func(context.Context, *Claims) context.Context) connect.Interceptor
ConnectInterceptor returns a full connect.Interceptor (unary + streaming handler) that authenticates each request and stores the resulting claims on ctx via withClaims.
Use this in preference to Validator.Interceptor for any new wiring — connect-go silently bypasses connect.UnaryInterceptorFunc for streaming RPCs, which would otherwise leave streaming endpoints unauthenticated.
Streaming clients are pass-through: the auth interceptor is server-side.
func (*Validator) Interceptor ¶
func (v *Validator) Interceptor(opts InterceptorOptions, withClaims func(context.Context, *Claims) context.Context) connect.UnaryInterceptorFunc
Interceptor returns a unary-only Connect interceptor that authenticates each request and stores the resulting claims in the context using the supplied claims-context helper.
withClaims is the function the user's pkg/middleware exposes for putting claims into context (typically middleware.ContextWithClaims).
This entrypoint is kept for backwards compatibility with projects that wired auth as a connect.UnaryInterceptorFunc. New code should prefer Validator.ConnectInterceptor, which also covers streaming RPCs.
func (*Validator) IsUnauthenticatedProcedure ¶
IsUnauthenticatedProcedure reports whether procedure should bypass auth. This includes the built-in gRPC health-check skip plus any explicit skip list.
func (*Validator) SetKeyValidator ¶
func (v *Validator) SetKeyValidator(kv KeyValidator)
SetKeyValidator replaces the configured KeyValidator. Useful when the validator is constructed before the storage backend is ready.