auth

package
v0.1.13 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BearerToken

func BearerToken(header string) (token string, ok bool)

BearerToken extracts the token from an Authorization header value.

The auth-scheme is matched CASE-INSENSITIVELY, because RFC 9110 §11.1 says it is: "the scheme is case-insensitive". Every site in the engine used to compare against the literal "Bearer " with strings.HasPrefix, so a perfectly legal `Authorization: bearer <valid token>` was answered 401 {"error":"invalid token"} — the engine rejected a correct request and told the caller their token was bad. Verified live before the fix (ADR-024):

Bearer <tok>  → 200
bearer <tok>  → 401 invalid token
BEARER <tok>  → 401 invalid token

RFC 9110 also allows more than one space between the scheme and the credentials (`credentials = auth-scheme 1*SP token68`), so leading spaces are trimmed rather than assuming exactly one.

ok is false when the header is empty or carries a DIFFERENT scheme (Basic, Negotiate, …). Callers distinguish that from a malformed token so the error can say which of the two happened — "invalid token" is a lie when the caller never sent a Bearer token at all.

This is the single parser for the whole engine. It exists as one function because the same three lines were duplicated at eight call sites (the JWT middleware twice, the platform admin twice, tenant auth twice, the response cache, and the admin tenant-token check); a fix applied to some of them and not the rest would be worse than the bug, since the response cache keys on the token it extracts and would simply stop caching for any caller the middleware had started accepting.

func GenerateToken

func GenerateToken(c Claims, secret string) (string, error)

GenerateToken signs a HS256 JWT that expires in 24h. The caller fills UserID/Role/TenantID; expiry and issued-at are set here.

func GenerateTokenWithTTL

func GenerateTokenWithTTL(c Claims, secret string, ttl time.Duration) (string, error)

GenerateTokenWithTTL signs a HS256 JWT that expires in ttl. Unlike GenerateToken (fixed 24h), it lets a caller mint a SHORT-LIVED token — e.g. the outbox worker minting a 60s service token per write-back operation, so no long-lived credential exists to leak (ADR-016 §Class 2 write-back). It sets iat/exp but PRESERVES any RegisteredClaims the caller pre-filled (e.g. Subject "service:worker" for audit), and emits the exact same Claims shape ValidateToken accepts — there is only ONE claims contract.

func HookUserContext

func HookUserContext(ctx context.Context) map[string]any

HookUserContext builds the `user` binding passed to a lifecycle hook from the request's JWT claims (SEC-AUDIT-V2 Hallazgo B): so a before_create/before_update js/wasm hook can see WHO performed the operation (`user.user_id`, `user.role`, `user.tenant_id`). Returns nil when there are no claims (e.g. an internal call without a JWT) — the hook's `user` is then nil, the pre-fix behavior. The SAME shape is used on REST and GraphQL so a hook is portable across both surfaces.

func JWTMiddleware

func JWTMiddleware(secret string, onError ...func(tenantID, reason string)) func(http.Handler) http.Handler

JWTMiddleware validates Bearer tokens on all routes except those in skipJWT. 401 is returned for missing or invalid tokens on enforced routes. Optional onError callback (tenantID, reason) is called on every 401 so callers can forward auth failures to an error store without importing auth from observability.

func JWTMiddlewareWithAnonymous added in v0.1.5

func JWTMiddlewareWithAnonymous(secret string, isPublic PublicMatcher, isStatic func(path string) bool, anonRole string, onError ...func(tenantID, reason string)) func(http.Handler) http.Handler

JWTMiddlewareWithAnonymous is JWTMiddlewareWithStatic plus the DECLARATIVE anonymous surface (ADR-026, PUBLIC-SURFACE-S1): when anonRole is non-empty (the app's schema declares rbac.public), a request with NO Authorization header on an otherwise-enforced route proceeds carrying synthetic claims {Role: anonRole, TenantID: <request tenant>} — downstream RBAC then decides per resource with the one existing evaluator, deny-by-default. The ENG-6 rule holds unchanged: a PRESENT-but-invalid/expired/foreign-tenant Bearer is a 401, never a silent downgrade to anonymous. Empty anonRole is byte- identical to JWTMiddlewareWithStatic.

func JWTMiddlewareWithPublic

func JWTMiddlewareWithPublic(secret string, isPublic PublicMatcher, onError ...func(tenantID, reason string)) func(http.Handler) http.Handler

JWTMiddlewareWithPublic is JWTMiddleware plus OPTIONAL authentication for the custom routes explicitly registered as Public (appximo.Route{Public: true}). Public means "no token required", not "identity ignored" (LIBRARY-GAPS-S2, ENG-6): with no Authorization header the request proceeds anonymous (Claims zero / ClaimsFromCtx nil); with a VALID Bearer the claims are populated so a handler can personalize (a checkout that recognizes a logged-in customer); with a present-but-invalid, expired or tenant-mismatched Bearer the request is 401ed — sent credentials never silently degrade to anonymous. Everything not matched by isPublic keeps the full Bearer enforcement: deny-by-default is unchanged for every other route.

func JWTMiddlewareWithStatic

func JWTMiddlewareWithStatic(secret string, isPublic PublicMatcher, isStatic func(path string) bool, onError ...func(tenantID, reason string)) func(http.Handler) http.Handler

JWTMiddlewareWithStatic is JWTMiddlewareWithPublic plus a PER-APP predicate for user-declared static mounts (appximo.Config.Static, LOOSE-ENDS-SWEEP-S1). A frontend's HTML/JS loads before any token exists — exactly like /editor and /admin, which are in the fixed skipJWT list — but a user mount is only known at boot, so it is passed in rather than hardcoded.

It is a PREDICATE rather than a prefix because a root-mounted SPA cannot be expressed as a prefix (the prefix "/" would disable authentication for /api), and a PARAMETER rather than package state so two apps in one fleet process never inherit each other's mounts. nil behaves byte-identically to JWTMiddlewareWithPublic.

func SetCachedClaims

func SetCachedClaims(secret, token string, c *Claims)

SetCachedClaims stores validated claims for a (secret, token). Exported for testing.

func StartClaimsCacheGC

func StartClaimsCacheGC(ctx context.Context)

StartClaimsCacheGC periodically evicts expired entries. Lookups already delete entries lazily on access, but a token that is never presented again would otherwise linger until process exit; over a long uptime with token rotation that is an unbounded (if slow) leak. Only validated tokens are ever cached, so this is not attacker-floodable — the sweep just keeps the footprint tidy. Runs until ctx is cancelled.

Types

type Claims

type Claims struct {
	UserID           string `json:"user_id"`
	Role             string `json:"role"`
	ExternalClientID string `json:"external_client_id,omitempty"`
	TenantID         string `json:"tenant_id"`
	jwt.RegisteredClaims
}

Claims is the payload embedded in every Appximo JWT.

func ClaimsFromCtx

func ClaimsFromCtx(ctx context.Context) *Claims

ClaimsFromCtx retrieves the Claims injected by JWTMiddleware. Returns nil when the middleware was not applied or the path was not enforced.

func GetCachedClaims

func GetCachedClaims(secret, token string) (*Claims, bool)

GetCachedClaims looks up previously validated claims for a Bearer token, SCOPED to the validating secret (MT-STRUCT-S3). The secret is part of the cache key because with N apps in one process — each validating with its OWN JWT secret — a token-only key would let a token validated by app X be served from cache to app Y, silently bypassing the signature check with secret_Y (a cross-app auth hole). Scoping the key makes a cache hit mean exactly "this secret already validated this token". Returns nil, false on miss or expiry.

func ValidateToken

func ValidateToken(tokenStr, secret string) (*Claims, error)

ValidateToken parses and validates a signed JWT string. Returns an error if the token is expired, malformed, or signed with a different secret.

type PublicMatcher

type PublicMatcher func(method, path string) bool

PublicMatcher reports whether (method, path) is an EXPLICITLY-registered public custom route (LIBRARY-EXTEND-S1: Route.Public). Matching is exact — method + literal path, never a prefix — so marking one route public can never widen the skip to a sibling. nil means "no public routes" (the default), and the middlewares below pay only a nil check for it.

Jump to

Keyboard shortcuts

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