Documentation
¶
Overview ¶
Package middleware provides HTTP middleware for authentication and authorization.
Package middleware provides HTTP middleware for SystemForge session management.
Index ¶
- Constants
- Variables
- func APIKeyMiddleware(config APIKeyMiddlewareConfig) func(http.Handler) http.Handler
- func ChiAuth(jwtService *jwt.Service, cfg DPoPConfig) (func(http.Handler) http.Handler, error)
- func ChiAuthOptional(jwtService *jwt.Service, cfg DPoPConfig) (func(http.Handler) http.Handler, error)
- func ChiRequireAnyPermission(permissions ...string) func(http.Handler) http.Handler
- func ChiRequireAnyRole(roles ...string) func(http.Handler) http.Handler
- func ChiRequireOrganization() func(http.Handler) http.Handler
- func ChiRequirePermission(permission string) func(http.Handler) http.Handler
- func ChiRequirePlatformAdmin() func(http.Handler) http.Handler
- func ChiRequireRole(role string) func(http.Handler) http.Handler
- func ClaimsFromContext(ctx context.Context) *jwt.Claims
- func ContextWithClaims(ctx context.Context, claims *jwt.Claims) context.Context
- func GetAPIKey(ctx context.Context) *apikey.APIKey
- func HTTPAuth(jwtService *jwt.Service, cfg DPoPConfig) (func(http.Handler) http.Handler, error)
- func HTTPAuthBearer(jwtService *jwt.Service) func(http.Handler) http.Handler
- func HTTPAuthBearerForAudience(jwtService *jwt.Service, audiences ...string) func(http.Handler) http.Handler
- func HTTPAuthBearerOptional(jwtService *jwt.Service) func(http.Handler) http.Handler
- func HTTPAuthBearerOptionalWithObservability(jwtService *jwt.Service, obs *observability.Observability) func(http.Handler) http.Handler
- func HTTPAuthBearerWithObservability(jwtService *jwt.Service, obs *observability.Observability) func(http.Handler) http.Handler
- func HTTPAuthForAudience(jwtService *jwt.Service, cfg DPoPConfig, audiences ...string) (func(http.Handler) http.Handler, error)
- func HTTPAuthOptional(jwtService *jwt.Service, cfg DPoPConfig) (func(http.Handler) http.Handler, error)
- func HasAllPermissions(ctx context.Context, permissions ...string) bool
- func HasAnyPermission(ctx context.Context, permissions ...string) bool
- func HasAnyRole(ctx context.Context, roles ...string) bool
- func HasPermission(ctx context.Context, permission string) bool
- func HasRole(ctx context.Context, role string) bool
- func IsPlatformAdminFromContext(ctx context.Context) bool
- func OptionalAPIKey(service *apikey.Service, ownerCheck APIKeyOwnerCheck) func(http.Handler) http.Handler
- func OrganizationIDFromContext(ctx context.Context) *uuid.UUID
- func PermissionsFromContext(ctx context.Context) []string
- func PrincipalIDFromContext(ctx context.Context) uuid.UUID
- func PrincipalTypeFromContext(ctx context.Context) string
- func RequireAPIKey(service *apikey.Service, ownerCheck APIKeyOwnerCheck) func(http.Handler) http.Handler
- func RequireAPIKeyWithScopes(service *apikey.Service, ownerCheck APIKeyOwnerCheck, scopes ...string) func(http.Handler) http.Handler
- func RequireAnyPermission(permissions ...string) func(http.Handler) http.Handler
- func RequireAnyRole(roles ...string) func(http.Handler) http.Handler
- func RequireAnyScope(scopes ...string) func(http.Handler) http.Handler
- func RequireOrganization() func(http.Handler) http.Handler
- func RequirePermission(permission string) func(http.Handler) http.Handler
- func RequirePlatformAdmin() func(http.Handler) http.Handler
- func RequireRole(role string) func(http.Handler) http.Handler
- func RequireScope(scope string) func(http.Handler) http.Handler
- func RoleFromContext(ctx context.Context) string
- func UserIDFromContext(ctx context.Context) uuid.UUID
- type APIKeyMiddlewareConfig
- type APIKeyOwnerCheck
- type DPoPAuth
- type DPoPConfig
- type ErrorResponse
- type MemoryReplayCache
- type Principal
- type RedisReplayCache
- type ReplayCache
Constants ¶
const ( // ContextKeyAPIKey is the context key for the validated API key. ContextKeyAPIKey contextKey = "api_key" // ContextKeyPrincipal is the context key for the authenticated principal. ContextKeyPrincipal contextKey = "principal" )
Variables ¶
var ( // ErrDPoPProofRequired is returned when a DPoP-bound access token // (one with a cnf.jkt claim) is presented without a valid DPoP proof, // including as a plain Bearer token. ErrDPoPProofRequired = errors.New("DPoP-bound token requires the DPoP scheme and a DPoP proof") // ErrDPoPTokenNotBound is returned when a token without a cnf.jkt claim // is presented with the DPoP scheme. ErrDPoPTokenNotBound = errors.New("token presented with the DPoP scheme is not DPoP-bound") // ErrDPoPProofReplayed is returned for a proof whose jti was already // used. ErrDPoPProofReplayed = errors.New("DPoP proof replayed") // ErrBearerTokenRefused is returned for an unbound token (no cnf.jkt) // when DPoPConfig.AllowBearer is not set. ErrBearerTokenRefused = errors.New("access token is not DPoP-bound; bearer tokens are not accepted") // ErrMissingAuthorization is returned for a request without an access // token in the Bearer or DPoP scheme. ErrMissingAuthorization = errors.New("missing authorization header") // ErrReplayCacheRequired is returned by NewDPoPAuth without a replay // cache. ErrReplayCacheRequired = errors.New("DPoPConfig.ReplayCache is required") )
var ErrAPIKeyOwnerNotAllowed = errors.New("API key owner no longer has access")
ErrAPIKeyOwnerNotAllowed is returned by an APIKeyOwnerCheck when a valid key's owner may no longer use it: the owner is deactivated or deleted, or is no longer a member of the key's organization.
Functions ¶
func APIKeyMiddleware ¶
func APIKeyMiddleware(config APIKeyMiddlewareConfig) func(http.Handler) http.Handler
APIKeyMiddleware creates middleware that validates API keys and checks, with config.OwnerCheck, that each key's owner still has access. It panics when config.Service or config.OwnerCheck is nil.
func ChiAuth ¶
ChiAuth returns a Chi-compatible middleware that validates DPoP-bound access tokens. This is an alias for HTTPAuth since Chi uses the standard http.Handler interface.
Usage with Chi:
auth, err := middleware.ChiAuth(jwtService, middleware.DPoPConfig{ReplayCache: cache})
if err != nil { ... }
r := chi.NewRouter()
r.Use(auth)
r.Get("/api/protected", protectedHandler)
func ChiAuthOptional ¶
func ChiAuthOptional(jwtService *jwt.Service, cfg DPoPConfig) (func(http.Handler) http.Handler, error)
ChiAuthOptional returns a Chi-compatible middleware that validates DPoP-bound access tokens if present. This is an alias for HTTPAuthOptional since Chi uses the standard http.Handler interface.
func ChiRequireAnyPermission ¶
ChiRequireAnyPermission returns a Chi-compatible middleware that requires any of the specified permissions.
func ChiRequireAnyRole ¶
ChiRequireAnyRole returns a Chi-compatible middleware that requires any of the specified roles.
func ChiRequireOrganization ¶
ChiRequireOrganization returns a Chi-compatible middleware that requires organization context.
func ChiRequirePermission ¶
ChiRequirePermission returns a Chi-compatible middleware that requires a specific permission.
func ChiRequirePlatformAdmin ¶
ChiRequirePlatformAdmin returns a Chi-compatible middleware that requires platform admin status.
func ChiRequireRole ¶
ChiRequireRole returns a Chi-compatible middleware that requires a specific role.
Usage with Chi:
r.Group(func(r chi.Router) {
r.Use(auth) // from middleware.ChiAuth
r.Use(middleware.ChiRequireRole("admin"))
r.Get("/api/admin", adminHandler)
})
func ClaimsFromContext ¶
ClaimsFromContext extracts JWT claims from the context. Returns nil if no claims are present.
func ContextWithClaims ¶
ContextWithClaims returns a new context with the JWT claims attached.
func HTTPAuth ¶
HTTPAuth returns middleware that validates access tokens and attaches their claims to the request context. It requires DPoP (RFC 9449): a token must be bound to a key (cnf.jkt) and come with the "DPoP" scheme and a valid proof by that key; see NewDPoPAuth. Unbound bearer tokens are refused unless cfg.AllowBearer is set. Only access tokens addressed to the service's Config.Audience are accepted.
func HTTPAuthBearer ¶ added in v0.21.0
HTTPAuthBearer returns middleware that validates unbound bearer JWTs: it extracts the token from the Authorization header (Bearer scheme) and attaches the claims to the request context. Only access tokens addressed to the service's Config.Audience are accepted, and a DPoP-bound token is refused. Prefer HTTPAuth, which requires DPoP.
func HTTPAuthBearerForAudience ¶ added in v0.21.0
func HTTPAuthBearerForAudience(jwtService *jwt.Service, audiences ...string) func(http.Handler) http.Handler
HTTPAuthBearerForAudience is HTTPAuthBearer for routes that serve one kind of client: it admits only access tokens addressed to one of the audiences (for example jwt.AudienceBFF on browser routes), so a token minted for another audience, such as a narrowly scoped API token, cannot be used as a full session. Prefer HTTPAuthForAudience, which requires DPoP.
func HTTPAuthBearerOptional ¶ added in v0.21.0
HTTPAuthBearerOptional is HTTPAuthBearer that lets requests without a valid token through without claims. Prefer HTTPAuthOptional, which requires DPoP.
func HTTPAuthBearerOptionalWithObservability ¶ added in v0.21.0
func HTTPAuthBearerOptionalWithObservability(jwtService *jwt.Service, obs *observability.Observability) func(http.Handler) http.Handler
HTTPAuthBearerOptionalWithObservability is HTTPAuthBearerOptional with observability. If obs is nil, metrics are not recorded.
func HTTPAuthBearerWithObservability ¶ added in v0.21.0
func HTTPAuthBearerWithObservability(jwtService *jwt.Service, obs *observability.Observability) func(http.Handler) http.Handler
HTTPAuthBearerWithObservability is HTTPAuthBearer with observability. If obs is nil, metrics are not recorded.
func HTTPAuthForAudience ¶ added in v0.14.0
func HTTPAuthForAudience(jwtService *jwt.Service, cfg DPoPConfig, audiences ...string) (func(http.Handler) http.Handler, error)
HTTPAuthForAudience is HTTPAuth for routes that serve one kind of client: it admits only access tokens addressed to one of the audiences (for example jwt.AudienceBFF on browser routes), so a token minted for another audience, such as a narrowly scoped API token, cannot be used as a full session.
func HTTPAuthOptional ¶
func HTTPAuthOptional(jwtService *jwt.Service, cfg DPoPConfig) (func(http.Handler) http.Handler, error)
HTTPAuthOptional is HTTPAuth that lets requests without valid credentials through without claims.
func HasAllPermissions ¶
HasAllPermissions checks if the user has all of the specified permissions.
func HasAnyPermission ¶
HasAnyPermission checks if the user has any of the specified permissions.
func HasAnyRole ¶
HasAnyRole checks if the user has any of the specified roles.
func HasPermission ¶
HasPermission checks if the user has a specific permission.
func IsPlatformAdminFromContext ¶
IsPlatformAdminFromContext checks if the user is a platform admin. Returns false if no claims are present.
func OptionalAPIKey ¶
func OptionalAPIKey(service *apikey.Service, ownerCheck APIKeyOwnerCheck) func(http.Handler) http.Handler
OptionalAPIKey creates middleware that validates API keys if present but doesn't require them. A key that is invalid, or whose owner fails ownerCheck, leaves the request unauthenticated. It panics when service or ownerCheck is nil.
func OrganizationIDFromContext ¶
OrganizationIDFromContext extracts the organization ID from the context. Returns nil if no organization context is present.
func PermissionsFromContext ¶
PermissionsFromContext extracts the user's permissions from the context. Returns nil if no permissions are present.
func PrincipalIDFromContext ¶
PrincipalIDFromContext extracts the principal ID from the context. Returns uuid.Nil if no claims or principal ID is present.
func PrincipalTypeFromContext ¶
PrincipalTypeFromContext extracts the principal type from the context. Returns empty string if no claims are present.
func RequireAPIKey ¶
func RequireAPIKey(service *apikey.Service, ownerCheck APIKeyOwnerCheck) func(http.Handler) http.Handler
RequireAPIKey creates middleware that requires a valid API key whose owner passes ownerCheck. This is a convenience function with default configuration.
func RequireAPIKeyWithScopes ¶
func RequireAPIKeyWithScopes(service *apikey.Service, ownerCheck APIKeyOwnerCheck, scopes ...string) func(http.Handler) http.Handler
RequireAPIKeyWithScopes creates middleware that requires specific scopes.
func RequireAnyPermission ¶
RequireAnyPermission returns middleware that requires any of the specified permissions. Must be used after HTTPAuth (or HTTPAuthBearer) middleware.
func RequireAnyRole ¶
RequireAnyRole returns middleware that requires the user to have any of the specified roles. Must be used after HTTPAuth (or HTTPAuthBearer) middleware.
func RequireAnyScope ¶
RequireAnyScope creates middleware that checks for any of the given scopes.
func RequireOrganization ¶
RequireOrganization returns middleware that requires an organization context. Must be used after HTTPAuth (or HTTPAuthBearer) middleware.
func RequirePermission ¶
RequirePermission returns middleware that requires a specific permission. Must be used after HTTPAuth (or HTTPAuthBearer) middleware.
func RequirePlatformAdmin ¶
RequirePlatformAdmin returns middleware that requires platform admin status. Must be used after HTTPAuth (or HTTPAuthBearer) middleware.
func RequireRole ¶
RequireRole returns middleware that requires the user to have a specific role. Must be used after HTTPAuth (or HTTPAuthBearer) middleware.
func RequireScope ¶
RequireScope creates middleware that checks for a specific scope. This should be used after APIKeyMiddleware.
func RoleFromContext ¶
RoleFromContext extracts the user's role from the context. Returns empty string if no role is present.
Types ¶
type APIKeyMiddlewareConfig ¶
type APIKeyMiddlewareConfig struct {
// Service is the API key service.
Service *apikey.Service
// OwnerCheck confirms that the key's owner still has access
// (required). APIKeyMiddleware panics without it.
OwnerCheck APIKeyOwnerCheck
// RequiredScopes are scopes that must be present (all required).
RequiredScopes []string
// AnyScopes requires at least one of these scopes.
AnyScopes []string
// HeaderName is the header containing the API key.
// Default: "Authorization" with "Bearer" scheme.
HeaderName string
// AllowQueryParam enables API key in query parameter.
// Default: false (more secure).
AllowQueryParam bool
// QueryParamName is the query parameter name.
// Default: "api_key".
QueryParamName string
// RecordUsage updates the last used timestamp on each request.
// Default: true.
RecordUsage bool
// OnError is called when authentication fails.
// If nil, returns 401 Unauthorized.
OnError func(w http.ResponseWriter, r *http.Request, err error)
// OnSuccess is called after successful authentication.
OnSuccess func(r *http.Request, key *apikey.APIKey)
// IPExtractor extracts the client IP from the request.
// If nil, uses r.RemoteAddr.
IPExtractor func(r *http.Request) string
// Observability is the observability provider for metrics.
// If nil, no metrics are recorded.
Observability *observability.Observability
}
APIKeyMiddlewareConfig contains configuration for the API key middleware.
func DefaultAPIKeyMiddlewareConfig ¶
func DefaultAPIKeyMiddlewareConfig() APIKeyMiddlewareConfig
DefaultAPIKeyMiddlewareConfig returns default configuration.
type APIKeyOwnerCheck ¶ added in v0.21.0
APIKeyOwnerCheck confirms, on every request, that the owner of a valid key may still use it. It must check that key.OwnerID is an active principal and, when key.OrganizationID is set, that the owner is still an active member of that organization, and return ErrAPIKeyOwnerNotAllowed (possibly wrapped) when not. Any error refuses the request.
A key's validity only proves who created it and what it was scoped to then; without this check, a key keeps asserting its organization after its owner was removed from it or deactivated.
type DPoPAuth ¶ added in v0.21.0
type DPoPAuth struct {
// contains filtered or unexported fields
}
DPoPAuth validates access tokens and enforces DPoP sender constraint.
func NewDPoPAuth ¶ added in v0.21.0
func NewDPoPAuth(jwtService *jwt.Service, cfg DPoPConfig) (*DPoPAuth, error)
NewDPoPAuth returns DPoP-aware token authentication. A token bound to a DPoP key (cnf.jkt) is accepted only with the "DPoP" authorization scheme and a proof that is signed by that key, matches the request method and URI, carries the hash of the token (ath), is fresh, and has not been used before. A token without cnf.jkt is refused unless DPoPConfig.AllowBearer is set, and then accepted only with the "Bearer" scheme.
func (*DPoPAuth) Authenticate ¶ added in v0.21.0
Authenticate validates the request's access token and, for a DPoP-bound token, its DPoP proof, and returns the token's claims.
func (*DPoPAuth) Middleware ¶ added in v0.21.0
Middleware returns middleware that authenticates the request and attaches the token's claims to the context, accepting only tokens addressed to one of audiences (all of the service's Config.Audience when none are given).
func (*DPoPAuth) OptionalMiddleware ¶ added in v0.21.0
OptionalMiddleware is Middleware that lets a request without valid credentials through without claims, for routes that serve anonymous and signed-in users alike.
type DPoPConfig ¶ added in v0.21.0
type DPoPConfig struct {
// Verification sets the proof age window and clock skew. Access token
// binding (the ath claim) is always required. Zero values use
// dpop.DefaultVerificationConfig.
Verification dpop.VerificationConfig
// ReplayCache rejects a proof whose jti has been used before.
// Required: use NewMemoryReplayCache for a single instance and
// NewRedisReplayCache when several instances serve the API.
ReplayCache ReplayCache
// BaseURL is the scheme and host clients use to reach this API, such as
// "https://api.example.com"; the proof's htu must equal BaseURL plus the
// request path. If empty, the scheme comes from the connection (TLS or
// not) and the host from the Host header. Forwarded headers are never
// trusted, so set BaseURL behind a proxy that terminates TLS.
BaseURL string
// AllowBearer also accepts unbound tokens (without cnf.jkt) with the
// Bearer scheme. It is an explicit opt-in for legacy clients: by
// default an unbound token is refused, since whoever holds a bearer
// token can use it. A DPoP-bound token is never accepted as Bearer.
AllowBearer bool
// Observability, when set, records token validation metrics.
Observability *observability.Observability
}
DPoPConfig configures resource-side DPoP verification (RFC 9449).
type ErrorResponse ¶
ErrorResponse represents an error response body.
type MemoryReplayCache ¶ added in v0.21.0
type MemoryReplayCache struct {
// contains filtered or unexported fields
}
MemoryReplayCache is an in-process ReplayCache, suitable when one instance serves the API.
func NewMemoryReplayCache ¶ added in v0.21.0
func NewMemoryReplayCache() *MemoryReplayCache
NewMemoryReplayCache returns an empty in-process replay cache.
type Principal ¶
type Principal struct {
// Type is "user" or "api_key".
Type string `json:"type"`
// ID is the principal's unique identifier.
ID uuid.UUID `json:"id"`
// UserID is the user's ID (same as ID for users, owner ID for API keys).
UserID uuid.UUID `json:"user_id"`
// OrganizationID is the organization context (optional).
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
// Scopes are the permissions granted to this principal.
Scopes []string `json:"scopes,omitempty"`
// Environment is "live" or "test" for API keys.
Environment string `json:"environment,omitempty"`
// Metadata contains additional principal data.
Metadata map[string]string `json:"metadata,omitempty"`
}
Principal represents an authenticated entity (user or API key).
func GetPrincipal ¶
GetPrincipal retrieves the authenticated principal from the request context.
type RedisReplayCache ¶ added in v0.21.0
type RedisReplayCache struct {
// contains filtered or unexported fields
}
RedisReplayCache is a ReplayCache shared through Redis, for APIs served by several instances.
func NewRedisReplayCache ¶ added in v0.21.0
func NewRedisReplayCache(client redis.UniversalClient, prefix string) *RedisReplayCache
NewRedisReplayCache returns a replay cache that stores keys in Redis under prefix (for example "dpop-jti:").
type ReplayCache ¶ added in v0.21.0
type ReplayCache interface {
// Use records key until expiresAt and reports whether it was already
// recorded (a replay).
Use(ctx context.Context, key string, expiresAt time.Time) (replayed bool, err error)
}
ReplayCache records DPoP proof identifiers so that each proof is accepted once. Implementations must be safe for concurrent use and, when several instances serve the same API, shared between them.