Auth/AuthZ Strategy for smithy-hono
Status (as-built). An early proposal — a thin
@smithy-hono/authpackage hanging off a generatedauthMiddlewarestub (createJwtAuth/createMongoAuth/createApiKeyAuth) — was NOT the path taken and has been removed from this document. Auth/authZ now ships as a full, first-party security pipeline in@smithy-hono/security-core, not as a pluggable JWT/Mongo middleware factory. The generatedauthMiddlewarestub the proposal depended on was retired (the route emitter no longer emits it; seesrc/test/java/com/smithyhono/RouteEmitterTest.java). The current design is documented below.
Current Implementation (as-built)
Authentication and authorization are implemented in @smithy-hono/security-core
(packages/security-core/src/**), Web-standard APIs only (ARCH-01: no node:*,
no Buffer, no module-level env reads — config is injected, ARCH-05).
The pipeline
createSecurityPipeline(OPERATIONS, config) (pipeline/index.ts) composes the
canonical pre-deserialization middleware stack, mounted app-wide
(app.use('*', ...createSecurityPipeline(...))). The twelve ordered slots,
outermost → innermost:
requestId(S9)structuredLogger(S9)errorSanitizer(S9)securityHeaders(S3)assertHttps(S3)cors(S8) — OPTIONS preflight short-circuits herebodyGuards(S4)rateLimitPerIp(S7)authenticate(S5) — setsc.get('principal')verifySignature(S6) — S2S (sigv4Hmac) ops onlycsrf(S8) — cookie-auth requests onlyrateLimitPerPrincipal(S7)
Optional OPS-04 DoS guards (loadShedder / withTimeout) mount between slot 6
and slot 7 when configured. The per-request operation is matched from the
codegen-emitted OPERATIONS registry (registry.gen.ts) via resolveOp — core
reads it structurally and never imports generated code.
Authentication (S5)
- OIDC cookie sessions — the browser flow.
auth/oidc.tsdoes OIDC discovery, caches a remote JWKS, and verifies an ID token's signature +iss/aud/exp/iat/nonceviajose(Web-Crypto). Verification returns a brandedVerifiedClaimstype that onlysessionFromOidcClaimsaccepts — a compile-time auth-bypass guard (RT-03).joseis the onlyjose-importing module and is tree-shakeable, so non-OIDC deploys never load it. - Server-authoritative sessions — the browser holds only an opaque id in a
__Host-cookie; the principal, the CSRF synchronizer token, and the absolute-expiry ceiling live in theSessionStore.auth/session.tsmints, rotates (rotate-on-privilege-change, AUTH-04), and derives those records; theauthenticatephase slides the idle TTL (AUTH-05) and sets bothprincipalandsessionon the context. - The
authenticatephase runs before body parsing (AUTH-11) and emits a uniform 401 on any failure (AUTH-10). S2S ops are deferred to S6.
Service-to-service signing (S6)
A custom SH-HMAC-SHA256 scheme (the @sigv4Hmac trait / sigv4Hmac auth
scheme). signing/signer.ts is a portable, Hono-free reference signer; the
verifySignature phase (signing/verifySignature.ts) re-derives the body hash
from the raw bytes (signing/rawBody.ts) and verifies against the same
canonicalization (signing/canonical.ts) — a round-trip proven in
signing/roundtrip.test.ts. Crypto is crypto.subtle HMAC only. Signing keys are
provisioned/rotated/revoked (with an overlap window) by @smithy-hono/key-tool.
Authorization (two-tier)
- Operation tier —
pipeline/authorize.ts.authorize(OPERATIONS.<Op>)is the codegen-emitted post-validation hook on every protected route; it enforces the operation'srequiredPermissionsdeny-by-default (401 no principal / 403AccessDeniedmissing permission, AUTHZ-01/02). - Resource tier —
authz/resourcePolicy.ts.requireResourcePolicy(...)rides the per-operation middleware slot (NO codegen, AUTHZ-09) and answers "may this principal act on this resource?" with zero-dep ABAC helpers (isOwner,sameTenant,all,any) plus a memoizing loader so the resource is fetched at most once (AUTHZ-03). The same interface accepts a ReBAC engine (OpenFGA / Cedar). Owner/tenant scoping is also expressed declaratively on@persistedresources via theownerField/tenantFieldtrait members (AUTHZ-07).
CSRF, CORS, headers, rate limiting
- CSRF (
pipeline/csrf.ts) — server-authoritative synchronizer-token check for cookie-authenticated state-changing requests;!sessionis the registry-free signal that a request is not cookie-authed and is skipped. - CORS (
pipeline/cors.ts) — config-injected origin allow-list with the preflight short-circuit and credentialed-CORS discipline; implemented directly rather than wrapping Hono'scorsso the allow-list is injected. - Security headers (
pipeline/headers.ts) — response headers + HTTPS assertion, route-class aware (@sseStreamops skipCache-Control: no-store). - Rate limiting (
pipeline/rateLimit.ts) — two token-bucket limiters (per-IP pre-auth, per-principal post-auth) honoring each operation's@cost, plusauthRateLimit(brute-force),withTimeout, andloadShedder. Backed by strongly-consistent store adapters (Redis Lua / DynamoDB CAS / Workers-KV).
Storage & adapters
Four injected storage interfaces — SessionStore, RateLimitStore,
NonceStore, SecretProvider (storage/index.ts, in-memory dev impls in
storage/memory.ts, conformance suite in storage/conformance.ts) — are
implemented by @smithy-hono/adapter-node (Redis), @smithy-hono/adapter-aws
(DynamoDB + Secrets Manager), and @smithy-hono/adapter-cf (Workers KV +
Durable Objects). examples/secure-api is the end-to-end wired reference.
Auth-related Smithy traits
Defined in model/traits.smithy (Java in src/main/java/com/smithyhono/traits/):
@requiresAuth(permission?)— operation requires authentication; the optional permission is checked against the principal (RequiresAuthTrait).@sigv4Hmac— marks an operation as requiring SH-HMAC S2S signing; surfaces as thesigv4Hmacauth scheme (Sigv4HmacTrait).@cost(value)— relative operation cost for the rate limiter (CostTrait).
(@persisted also carries the ownerField / tenantField scoping knobs used by
resource-tier authZ.)