@happy-technologies/auth
v0.1.0
Published
Shared Keycloak JWKS-cached RS256 verification, realm/client role extraction, and a pluggable RBAC guard factory. TypeScript core only; the Rust gateway verifier stays app-local and is kept in lockstep via the language-neutral conformance test-vector suit
Readme
@happy-technologies/auth
Shared Keycloak JWKS-cached RS256 verification, realm/client role
extraction, and a pluggable RBAC guard factory. Source-shipped ESM (no
build step, exports point at ./src/*), consumed as a caret semver
dependency, same connection model as @happy-technologies/audit and
@happy-technologies/design-system. jose is a peerDependency: the
consuming app owns the jose version.
Install
bun add @happy-technologies/auth joseUsage
import { createKeycloakVerifier, extractRealmRoles, createRbacGuard } from "@happy-technologies/auth";
const verifier = createKeycloakVerifier({
issuer: "https://auth.example.com/realms/happy",
jwksUrl: "https://auth.example.com/realms/happy/protocol/openid-connect/certs",
audience: "happy-app", // optional; enforced only when configured
clockTolerance: 30, // optional, seconds
});
const claims = await verifier.verify(bearerToken); // throws on any failure
const roles = extractRealmRoles(claims);
const guard = createRbacGuard({
roleMap: {
requiredFor: (req) => (req.path === "/v1/settings" ? "admin" : req.method === "GET" ? null : "write"),
satisfies: (roles, required) => {
const canAdmin = roles.includes("admin") || roles.includes("owner");
return required === "admin" ? canAdmin : canAdmin || roles.includes("editor");
},
},
onDeny: ({ roles, required, request }) => recordDenial(roles, required, request),
});
const decision = await guard.guard(claims, { method: "POST", path: "/v1/ingest" });
if (!decision.allowed) throw new Error("forbidden");extractRoles(claims, { includeResourceAccess: true, clientId }) additionally
unions in resource_access[clientId].roles for apps that read client-scoped
Keycloak roles.
Non-goals
This package intentionally does NOT define specific role tiers, route-to-role
tables, dual-issuer routing (e.g. Keycloak + Frappe), org/tenant selection
semantics, or any web-framework glue (Hono/Express adapters stay app-local).
The roleMap passed to createRbacGuard is entirely app-supplied; both a
3-tier map (admin/write/read) and an unrelated 4-tier map are expressible
without any change to this package (see src/index.test.ts).
Polyglot boundary (decision b2)
The platform also runs an app-local Rust gateway verifier (keycloak.rs,
rbac.rs). Rather than publish a Rust crate mirroring this TypeScript
package (decision b1, rejected), this package ships a language-neutral
conformance test-vector suite under conformance/:
conformance/jwks.json: a fixed RSA public JWKS.conformance/vectors.json: the same JWKS plusissuer,audience, and a set of signed JWT test cases, each with an expected verify outcome ({ valid: true }or{ valid: false, reason }).
Both the TypeScript verifier here (src/index.test.ts) and the app-local
Rust gateway verifier run against this same JSON file in their own CI. Since
both implementations are graded against one shared, versioned fixture set,
any divergence in issuer/audience/algorithm/claim handling between the two
languages fails a test rather than silently shipping. The Rust side lives
and is tested entirely in its own app repo; this package only owns
generating and committing the vectors (bun run gen:vectors).
Conformance cases
| name | expected | what it exercises |
| --- | --- | --- |
| valid | valid: true | correct issuer, audience, RS256 signature, not expired |
| wrong-issuer | valid: false, reason: "issuer" | iss claim mismatch |
| wrong-audience | valid: false, reason: "audience" | aud claim mismatch |
| expired | valid: false, reason: "expired" | exp in the past |
| wrong-algorithm | valid: false, reason: "algorithm" | signed HS256 instead of RS256 |
Regenerate with bun run gen:vectors (fresh RSA keypair each run; commit the
regenerated conformance/*.json).
Scripts
bun test- runssrc/*.test.ts, including the conformance suite.bunx tsc --noEmit- type-check only (no build; this package ships source).bun run check:no-em-dash- fails if any committed file contains U+2014.bun run gen:vectors- regeneratesconformance/vectors.json+jwks.json.
