From 063206d8b098e96a395f1f905127d918d03d82ac Mon Sep 17 00:00:00 2001 From: sax3l Date: Wed, 4 Nov 2026 02:23:21 +0100 Subject: [PATCH] feat(auth): adopt @siax/auth-zitadel canonical claim normalizer --- package.json | 5 +++-- pnpm-lock.yaml | 15 +++++++++++++++ src/auth/zitadel.ts | 33 +++++++++++++++++++++++++-------- tests/auth/zitadel.test.ts | 1 + 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 3b0a2f1..d3162d7 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "@radix-ui/react-toggle-group": "1.1.19", "@radix-ui/react-tooltip": "1.2.16", "@siax/0pt-decision": "workspace:*", + "@siax/access": "workspace:*", + "@siax/auth-zitadel": "0.1.1", "@siax/charts": "^0.5.0", "@siax/collaboration": "0.3.0", "@siax/config": "^0.2.1", @@ -81,8 +83,7 @@ "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.2", - "zod": "3.25.76", - "@siax/access": "workspace:*" + "zod": "3.25.76" }, "devDependencies": { "@eslint/js": "^9.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ea7227..74cf9c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: '@siax/access': specifier: workspace:* version: link:packages/siax-access + '@siax/auth-zitadel': + specifier: 0.1.1 + version: 0.1.1 '@siax/charts': specifier: ^0.5.0 version: 0.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1549,6 +1552,12 @@ packages: '@siax/analytical-context@0.5.1': resolution: {integrity: sha512-bqc2NGPyzLVcu4VUgz1XYsyghSM4c3ck5gBVFSzeRYMhe2cj0nf5Fm5n/sFaOuhMaKdDnX/TIR2FO6f21lgufg==, tarball: https://git.siax.io/api/packages/sax3l/npm/%40siax%2Fanalytical-context/-/0.5.1/analytical-context-0.5.1.tgz} + '@siax/auth-core@0.1.0': + resolution: {integrity: sha512-oTfEO4S6pv3rQ6bSh44+fRWPiwGr9nYCf6uWSBF8g5PFUspW6nr4wKQZVAkLNwfC0Pu6o+CH8ltsuu+fY8gOQA==, tarball: https://git.siax.io/api/packages/sax3l/npm/%40siax%2Fauth-core/-/0.1.0/auth-core-0.1.0.tgz} + + '@siax/auth-zitadel@0.1.1': + resolution: {integrity: sha512-SmoBswG5P7szZIaETqmo1NN5RhdXLq2RDR/1wObfxHM2yUEXNSZQJznZoAb8z3lm/PvBgQhmnHiOHCfidN1ZQg==, tarball: https://git.siax.io/api/packages/sax3l/npm/%40siax%2Fauth-zitadel/-/0.1.1/auth-zitadel-0.1.1.tgz} + '@siax/charts@0.11.0': resolution: {integrity: sha512-IQnby+h6jVMaRAM7DcfUMuMbSUjd8B3+AGk9N4pXk2GDudMRMjt0bTtu75637Jc1Mf4iNonvgp8ytKlS7KdW4A==, tarball: https://git.siax.io/api/packages/sax3l/npm/%40siax%2Fcharts/-/0.11.0/charts-0.11.0.tgz} peerDependencies: @@ -4236,6 +4245,12 @@ snapshots: '@siax/analytical-context@0.5.1': {} + '@siax/auth-core@0.1.0': {} + + '@siax/auth-zitadel@0.1.1': + dependencies: + '@siax/auth-core': 0.1.0 + '@siax/charts@0.11.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3)': dependencies: '@siax/analytical-context': 0.5.1 diff --git a/src/auth/zitadel.ts b/src/auth/zitadel.ts index 5184710..c6dad27 100644 --- a/src/auth/zitadel.ts +++ b/src/auth/zitadel.ts @@ -50,6 +50,7 @@ import { type JWTPayload, type JWTVerifyGetKey, } from 'jose'; +import { normalizeZitadelClaims, ZitadelClaimError } from '@siax/auth-zitadel'; /** Default fleet Zitadel issuer (OIDC). Overridable via ZITADEL_ISSUER. */ export const DEFAULT_ZITADEL_ISSUER = 'https://id-customers.siax.io'; @@ -254,13 +255,29 @@ export async function verifyBearer( throw new BearerVerificationError('token_verification_failed', err); } - const sub = typeof payload.sub === 'string' ? payload.sub : undefined; - if (!sub || sub.length === 0) { - throw new BearerVerificationError('token_missing_sub'); + try { + const identity = normalizeZitadelClaims(payload, { + source: 'jwt', + tenantClaim: opts.tenantClaim ?? tenantClaimOverride(), + }); + return { + sub: identity.principal.subject, + tenantId: identity.tenant.tenantId, + claims: payload, + }; + } catch (err) { + if (err instanceof ZitadelClaimError) { + if (/sub/i.test(err.reason)) { + throw new BearerVerificationError('token_missing_sub'); + } + if (/tenant/i.test(err.reason)) { + throw new BearerVerificationError('token_missing_tenant_claim'); + } + if (/aud/i.test(err.reason)) { + throw new BearerVerificationError('token_invalid_audience'); + } + throw new BearerVerificationError('token_verification_failed', err); + } + throw err; } - const tenantId = deriveTenantId(payload, opts.tenantClaim ?? tenantClaimOverride()); - if (!tenantId) { - throw new BearerVerificationError('token_missing_tenant_claim'); - } - return { sub, tenantId, claims: payload }; } diff --git a/tests/auth/zitadel.test.ts b/tests/auth/zitadel.test.ts index e059208..24d4d1c 100644 --- a/tests/auth/zitadel.test.ts +++ b/tests/auth/zitadel.test.ts @@ -40,6 +40,7 @@ async function sign(claims: Record, opts: SignOpts = {}): Promi .setProtectedHeader({ alg: 'RS256', kid: KID }) .setIssuedAt() .setIssuer(opts.issuer ?? ISSUER) + .setAudience('siax-test') .setExpirationTime(opts.expirationTime ?? '2h') .sign(privateKey); } -- 2.54.0