feat(auth): implement @fuzefront/auth — real verifier + middleware (#117) - #285
Merged
Conversation
) @fuzefront/auth shipped as a contract freeze: every entry point threw VERIFIER_UNAVAILABLE. The authz guard literally responded "@fuzefront/auth is contract-frozen; authz guard runtime not yet implemented" So the package consuming services are told to install gave them NO way to verify a FuzeFront token or check a permission. Cross-service AuthN/AuthZ was a contract, not a capability. This makes it real. Public signatures are UNCHANGED from the v0.1.0 freeze — code written against the frozen types keeps compiling; it now also runs. Runtime (jose — one dependency covers HS256 today and RS256/JWKS for the target, so consumers don't pull two crypto libs): - legacy-hs256: today's session token; optional OutOfBandResolver hydrates the tenantId/roles the token doesn't carry. - federated-jwks: RS256/ES256 against the issuer's JWKS with iss/aud validation and OIDC discovery when jwksUri is omitted; JWKS cached per issuer. - requireAuth / requireRoles / requireTenant: real Express middleware. Security properties, made explicit rather than incidental: - Algorithms PINNED per mode. Unpinned `algorithms` is what enables `alg: none` and RS256-public-key-as-HMAC-secret confusion attacks. - tenantId: null means UNRESOLVED, never a wildcard — requireTenant denies. Treating null as "any tenant" would hand every tenant's data to any valid legacy token lacking a resolver. - A resolver failure DENIES rather than returning empty roles, which would be indistinguishable from a genuine permission decision and would mask an outage. - requireAuth never calls next() on an unauthenticated request; the promise is driven explicitly so a rejection denies rather than hanging the request. - 401 (authn) and 403 (authz) kept distinct so clients aren't sent into login loops over a permission problem. - Still never mints tokens; in federated-jwks mode a consumer holds no key. Tests: 31, weighted toward denial paths (algorithm confusion, forged signature, expiry, wrong issuer/audience, missing subject, resolver failure, unresolved tenant, guard-without-requireAuth). The package had NO test config at all — its tsconfig is build-shaped (types: ["node"], include: src only), so tests/tsconfig.json + jest.config.js were needed before a single test could run. Verified in a clean node:20 container: tsc --noEmit -> exit 0; jest -> 31/31 pass. Fixes #117. Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com> Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
The rule fires on `new SignJWT(...)` / `jwt.sign(...)` anywhere, with no path exclusions — so it flagged @fuzefront/auth's own test suite 10 times. Proving a verifier REJECTS a forged, expired, or wrong-issuer token requires minting exactly those tokens as fixtures. There is no other way to test it. The rule's own message says "replace it with FuzeFront token verification (@fuzefront/auth)" — and the code it flagged IS @fuzefront/auth. A rule that cries wolf on the verifier it points you to is one people learn to scroll past, which costs far more than the finding is worth. Excluding tests keeps it high-signal where it actually matters: production code minting user tokens, which is the thing the architecture forbids. Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com> Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #117.
@fuzefront/authwas a stub. Every entry point threwVERIFIER_UNAVAILABLE; the authz guard literally responded:So the package consuming services are told to install gave them no way to verify a FuzeFront token or check a permission. Cross-service AuthN/AuthZ was a contract, not a capability. This makes it real — installable as middleware.
Public signatures are unchanged from the v0.1.0 freeze: code written against the frozen types keeps compiling, it now also runs.
Install & use
Migrating to federated JWKS later is a config change, not a code change — swap
modetofederated-jwkswith anissuer.Runtime
One dependency —
josecovers HS256 today and RS256/JWKS for the target, so consumers don't pull two crypto libs.legacy-hs256— today's session token; optionalOutOfBandResolverhydrates thetenantId/rolesthe token doesn't carry.federated-jwks— RS256/ES256 against the issuer's JWKS withiss/audvalidation, OIDC discovery whenjwksUriis omitted, JWKS cached per issuer.requireAuth/requireRoles/requireTenant— real Express middleware.Security properties — deliberate, not incidental
algorithmsis exactly what enablesalg: noneand RS256-public-key-abused-as-HMAC-secret confusion. Tested.tenantId: nullmeans UNRESOLVED, never a wildcard.requireTenantdenies. Treating null as "any tenant" would hand every tenant's data to any valid legacy token lacking a resolver.requireAuthnever callsnext()on an unauthenticated request; the promise is driven explicitly so a rejection denies rather than hanging the request.federated-jwksmode a consumer holds no signing key at all — it can verify without gaining the power to forge.Tests — 31, weighted toward the denial paths
A false allow here is a family-wide breach; a false deny is an outage. So the happy path gets one test and the refusals get the rest: algorithm confusion, forged signature, expiry, wrong issuer, wrong audience, missing subject, resolver failure, unresolved tenant, and a guard mounted without
requireAuth.The package had no test config at all — its tsconfig is build-shaped (
types: ["node"],include: srconly), sotests/tsconfig.json+jest.config.jswere prerequisites before a single test could run. The federated tests serve a real JWKS over loopback rather than stubbingfetch, becausecreateRemoteJWKSetdoes its own fetching — that also exercises discovery for real.Verified (clean
node:20container, actually run)The type-checker caught a real gap mid-work:
IdentityrequiresauthMode, which I'd missed — now populated per verifier, along withissuedAt/claims.hold— master is deploy-on-push.Co-Authored-By: Claude