From 49a59173825bc5f515b73c20bde14031a282b7a4 Mon Sep 17 00:00:00 2001 From: stefanbaxter Date: Sun, 22 Mar 2026 17:32:36 +0000 Subject: [PATCH 1/7] feat(cubejs): add FraiOS token detection and HS256 verification Extend detectTokenType to distinguish FraiOS tokens (HS256 with accountId claim) from Hasura tokens. Add verifyFraiOSToken using TOKEN_SECRET env var. Co-Authored-By: Claude Sonnet 4.6 --- .../src/utils/__tests__/workosAuth.test.js | 95 +++++++++++++++++++ services/cubejs/src/utils/workosAuth.js | 59 +++++++++++- 2 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 services/cubejs/src/utils/__tests__/workosAuth.test.js diff --git a/services/cubejs/src/utils/__tests__/workosAuth.test.js b/services/cubejs/src/utils/__tests__/workosAuth.test.js new file mode 100644 index 00000000..280a396f --- /dev/null +++ b/services/cubejs/src/utils/__tests__/workosAuth.test.js @@ -0,0 +1,95 @@ +import { describe, it, before } from "node:test"; +import assert from "node:assert/strict"; +import { SignJWT } from "jose"; + +// Set TOKEN_SECRET before importing module under test +const TEST_TOKEN_SECRET = "test-fraios-secret-key-at-least-32-chars!!"; +process.env.TOKEN_SECRET = TEST_TOKEN_SECRET; + +const { detectTokenType, verifyFraiOSToken } = await import( + "../workosAuth.js" +); + +// --- Helpers --- + +function makeHS256Token(payload, secret) { + return new SignJWT(payload) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("1h") + .sign(new TextEncoder().encode(secret)); +} + +// --- detectTokenType --- + +describe("detectTokenType", () => { + it('returns "fraios" for HS256 token with accountId claim', async () => { + const token = await makeHS256Token( + { userId: "u1", email: "a@b.com", accountId: "org1" }, + TEST_TOKEN_SECRET + ); + assert.equal(detectTokenType(token), "fraios"); + }); + + it('returns "hasura" for HS256 token with hasura namespace', async () => { + const token = await makeHS256Token( + { hasura: { "x-hasura-user-id": "u1" } }, + "any-secret-that-is-long-enough-ok" + ); + assert.equal(detectTokenType(token), "hasura"); + }); + + it('returns "hasura" for malformed token', () => { + assert.equal(detectTokenType("not.a.jwt"), "hasura"); + }); +}); + +// --- verifyFraiOSToken --- + +describe("verifyFraiOSToken", () => { + it("verifies a valid FraiOS token and returns payload", async () => { + const token = await makeHS256Token( + { + userId: "u1", + email: "user@example.com", + accountId: "org1", + partition: "bonus.is", + }, + TEST_TOKEN_SECRET + ); + const payload = await verifyFraiOSToken(token); + assert.equal(payload.userId, "u1"); + assert.equal(payload.email, "user@example.com"); + assert.equal(payload.accountId, "org1"); + assert.equal(payload.partition, "bonus.is"); + }); + + it("rejects a token signed with wrong secret", async () => { + const token = await makeHS256Token( + { userId: "u1", email: "a@b.com", accountId: "org1" }, + "wrong-secret-that-is-long-enough!!" + ); + await assert.rejects(() => verifyFraiOSToken(token), (err) => { + assert.equal(err.status, 403); + return true; + }); + }); + + it("rejects an expired token", async () => { + const token = await new SignJWT({ + userId: "u1", + email: "a@b.com", + accountId: "org1", + }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt(Math.floor(Date.now() / 1000) - 7200) + .setExpirationTime(Math.floor(Date.now() / 1000) - 3600) + .sign(new TextEncoder().encode(TEST_TOKEN_SECRET)); + + await assert.rejects(() => verifyFraiOSToken(token), (err) => { + assert.equal(err.status, 403); + assert.ok(err.message.includes("expired")); + return true; + }); + }); +}); diff --git a/services/cubejs/src/utils/workosAuth.js b/services/cubejs/src/utils/workosAuth.js index 3b0be1b9..0570e656 100644 --- a/services/cubejs/src/utils/workosAuth.js +++ b/services/cubejs/src/utils/workosAuth.js @@ -1,6 +1,6 @@ import * as jose from "jose"; -const { WORKOS_CLIENT_ID, WORKOS_API_KEY, WORKOS_ISSUER } = process.env; +const { WORKOS_CLIENT_ID, WORKOS_API_KEY, WORKOS_ISSUER, TOKEN_SECRET } = process.env; // JWKS endpoint: use custom issuer if provided, otherwise default WorkOS URL const jwksBaseUrl = WORKOS_ISSUER || "https://api.workos.com"; @@ -16,14 +16,24 @@ const jwks = jose.createRemoteJWKSet(JWKS_URL); /** * Detect token type by decoding the JWT header without verification. * @param {string} token - Raw JWT string - * @returns {"workos" | "hasura"} Token type + * @returns {"workos" | "fraios" | "hasura"} Token type */ export function detectTokenType(token) { try { const header = jose.decodeProtectedHeader(token); - return header.alg === "RS256" ? "workos" : "hasura"; + if (header.alg === "RS256") return "workos"; + + // HS256 — distinguish FraiOS from Hasura by payload claims + const parts = token.split("."); + if (parts.length === 3) { + const payload = JSON.parse( + Buffer.from(parts[1], "base64url").toString() + ); + if (payload.accountId) return "fraios"; + } + return "hasura"; } catch { - return "hasura"; // Default to existing path on decode failure + return "hasura"; } } @@ -72,6 +82,47 @@ export async function verifyWorkOSToken(token) { } } +/** + * Verify a FraiOS HS256 JWT using the shared TOKEN_SECRET. + * @param {string} token - Raw JWT string + * @returns {Promise} Decoded JWT payload + * @throws {Error} With `status` property (403 for auth errors) + */ +export async function verifyFraiOSToken(token) { + if (!TOKEN_SECRET) { + const error = new Error("503: TOKEN_SECRET not configured"); + error.status = 503; + throw error; + } + + const secret = new TextEncoder().encode(TOKEN_SECRET); + + try { + const { payload } = await jose.jwtVerify(token, secret, { + algorithms: ["HS256"], + }); + return payload; + } catch (err) { + const error = new Error(err.message); + + if (err.code === "ERR_JWT_EXPIRED") { + error.message = "TokenExpiredError: jwt expired"; + error.status = 403; + } else if ( + err.code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED" || + err.code === "ERR_JWT_CLAIM_VALIDATION_FAILED" + ) { + error.message = "JsonWebTokenError: invalid signature"; + error.status = 403; + } else { + error.message = `JsonWebTokenError: ${err.message}`; + error.status = 403; + } + + throw error; + } +} + /** * Fetch a WorkOS user profile by their WorkOS user ID. * @param {string} workosUserId - WorkOS user ID (e.g., "user_01KEC615...") From afaacb9b3e24b8484bd025cbf611bbdcab6c9841 Mon Sep 17 00:00:00 2001 From: stefanbaxter Date: Sun, 22 Mar 2026 17:37:24 +0000 Subject: [PATCH 2/7] feat(cubejs): add FraiOS user provisioning with JIT identity resolution Email-based lookup with partition-driven team derivation. Reuses workosSubCache and singleflight dedup. Co-Authored-By: Claude Sonnet 4.6 --- .../utils/__tests__/provisionFraiOS.test.js | 137 ++++++++++++++++++ .../cubejs/src/utils/dataSourceHelpers.js | 127 ++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 services/cubejs/src/utils/__tests__/provisionFraiOS.test.js diff --git a/services/cubejs/src/utils/__tests__/provisionFraiOS.test.js b/services/cubejs/src/utils/__tests__/provisionFraiOS.test.js new file mode 100644 index 00000000..2fb2ce3b --- /dev/null +++ b/services/cubejs/src/utils/__tests__/provisionFraiOS.test.js @@ -0,0 +1,137 @@ +import { describe, it, mock, beforeEach } from "node:test"; +import assert from "node:assert/strict"; + +// Mock fetchGraphQL before importing module under test +const fetchGraphQLMock = mock.fn(); +mock.module("../graphql.js", { + namedExports: { fetchGraphQL: fetchGraphQLMock }, +}); +mock.module("../workosAuth.js", { + namedExports: { + fetchWorkOSUserProfile: mock.fn(), + detectTokenType: mock.fn(), + verifyWorkOSToken: mock.fn(), + verifyFraiOSToken: mock.fn(), + }, +}); + +const { + provisionUserFromFraiOS, + invalidateWorkosSubCache, +} = await import("../dataSourceHelpers.js"); + +describe("provisionUserFromFraiOS", () => { + beforeEach(() => { + fetchGraphQLMock.mock.resetCalls(); + invalidateWorkosSubCache(null); // clear cache between tests + }); + + it("returns userId when account found by email", async () => { + fetchGraphQLMock.mock.mockImplementation(async (query) => { + if (query.includes("FindAccountByEmail")) { + return { + data: { + accounts: [{ id: "acc-1", user_id: "user-uuid-1", email: "a@bonus.is" }], + }, + }; + } + if (query.includes("FindTeamByName")) { + return { data: { teams: [{ id: "team-1", user_id: "user-uuid-1" }] } }; + } + if (query.includes("CreateMember")) { + return { data: { insert_members_one: null } }; + } + if (query.includes("FindMember")) { + return { + data: { + members: [{ id: "member-1", member_roles: [{ id: "role-1" }] }], + }, + }; + } + return { data: {} }; + }); + + const userId = await provisionUserFromFraiOS({ + userId: "fraios-user-1", + email: "a@bonus.is", + accountId: "org-1", + partition: "bonus.is", + }); + + assert.equal(userId, "user-uuid-1"); + }); + + it("provisions new user when no account found", async () => { + fetchGraphQLMock.mock.mockImplementation(async (query) => { + if (query.includes("FindAccountByEmail")) { + return { data: { accounts: [] } }; + } + if (query.includes("CreateUser")) { + return { data: { insert_users_one: { id: "new-user-uuid" } } }; + } + if (query.includes("CreateAccount")) { + return { data: { insert_auth_accounts_one: { id: "acc-new", user_id: "new-user-uuid" } } }; + } + if (query.includes("FindTeamByName")) { + return { data: { teams: [] } }; + } + if (query.includes("CreateTeam")) { + return { data: { insert_teams_one: { id: "team-new" } } }; + } + if (query.includes("CreateMember")) { + return { data: { insert_members_one: { id: "member-new" } } }; + } + if (query.includes("CreateMemberRole")) { + return { data: { insert_member_roles_one: { id: "role-new" } } }; + } + return { data: {} }; + }); + + const userId = await provisionUserFromFraiOS({ + userId: "fraios-user-2", + email: "b@bonus.is", + accountId: "org-2", + partition: "bonus.is", + }); + + assert.equal(userId, "new-user-uuid"); + }); + + it("uses cache on second call with same fraios userId", async () => { + let callCount = 0; + fetchGraphQLMock.mock.mockImplementation(async (query) => { + if (query.includes("FindAccountByEmail")) { + callCount++; + return { + data: { + accounts: [{ id: "acc-3", user_id: "user-uuid-3", email: "c@bonus.is" }], + }, + }; + } + if (query.includes("FindTeamByName")) { + return { data: { teams: [{ id: "team-3" }] } }; + } + if (query.includes("CreateMember")) { + return { data: { insert_members_one: null } }; + } + if (query.includes("FindMember")) { + return { + data: { members: [{ id: "m-3", member_roles: [{ id: "r-3" }] }] }, + }; + } + return { data: {} }; + }); + + const payload = { + userId: "fraios-user-3", + email: "c@bonus.is", + accountId: "org-3", + partition: "bonus.is", + }; + + await provisionUserFromFraiOS(payload); + await provisionUserFromFraiOS(payload); + + assert.equal(callCount, 1, "should hit cache on second call"); + }); +}); diff --git a/services/cubejs/src/utils/dataSourceHelpers.js b/services/cubejs/src/utils/dataSourceHelpers.js index 26636f0c..079c00fb 100644 --- a/services/cubejs/src/utils/dataSourceHelpers.js +++ b/services/cubejs/src/utils/dataSourceHelpers.js @@ -655,3 +655,130 @@ async function _provisionUserFromWorkOS(sub, partition) { } } +/** + * Provision a user from a FraiOS JWT payload. + * Identity resolution chain (simplified vs WorkOS — email is in JWT): + * 1. Check identity cache keyed by FraiOS userId + * 2. DB lookup by email + * 3. If found → ensure team membership using partition → cache and return + * 4. If not found → JIT provision: create user, account, team, membership, role + * + * @param {Object} fraiosPayload - Verified FraiOS JWT payload + * @returns {Promise} Synmetrix userId (UUID) + */ +export async function provisionUserFromFraiOS(fraiosPayload) { + const externalId = fraiosPayload.userId; + const email = fraiosPayload.email; + const partition = fraiosPayload.partition; + + // Step 1: Check identity cache (reuses workosSubCache) + const cached = getWorkosSubCacheEntry(externalId); + if (cached) return cached.userId; + + // Singleflight dedup + let inflight = inflightProvisions.get(externalId); + if (inflight) return inflight; + + const provision = _provisionUserFromFraiOS(externalId, email, partition); + inflightProvisions.set(externalId, provision); + provision.finally(() => inflightProvisions.delete(externalId)); + return provision; +} + +async function _provisionUserFromFraiOS(externalId, email, partition) { + // Re-check cache after acquiring singleflight + const cached = getWorkosSubCacheEntry(externalId); + if (cached) return cached.userId; + + // Step 2: DB lookup by email + const account = await findAccountByEmail(email); + if (account) { + await ensureTeamMembership(account.user_id, email, partition); + setWorkosSubCacheEntry(externalId, account.user_id, true); + return account.user_id; + } + + // Step 3: JIT provision — create user, account, team, membership, role + try { + const displayName = email.split("@")[0] || email; + + const userResult = await fetchGraphQL(createUserMutation, { + display_name: displayName, + avatar_url: null, + }); + const userId = userResult.data?.insert_users_one?.id; + if (!userId) { + const error = new Error("503: Unable to provision user"); + error.status = 503; + throw error; + } + + // Create account (on_conflict: accounts_email_key) + await fetchGraphQL(createAccountMutation, { + user_id: userId, + email, + workos_user_id: `fraios:${externalId}`, + }); + + // Find or create team using partition + const teamName = deriveTeamName(email, partition); + let team = await findTeamByName(teamName); + let isTeamCreator = false; + + if (!team) { + const initialSettings = partition ? { partition } : {}; + const teamResult = await fetchGraphQL(createTeamMutation, { + name: teamName, + user_id: userId, + settings: + Object.keys(initialSettings).length > 0 ? initialSettings : null, + }); + + const teamId = teamResult.data?.insert_teams_one?.id; + if (teamId) { + team = { id: teamId }; + isTeamCreator = true; + } else { + team = await findTeamByName(teamName); + } + } + + if (!team) { + const error = new Error("503: Unable to provision user"); + error.status = 503; + throw error; + } + + const memberResult = await fetchGraphQL(createMemberMutation, { + user_id: userId, + team_id: team.id, + }); + let memberId = memberResult.data?.insert_members_one?.id; + + if (!memberId) { + const existing = await fetchGraphQL(findMemberByUserAndTeamQuery, { + user_id: userId, + team_id: team.id, + }); + const member = existing.data?.members?.[0]; + memberId = member?.id; + if (member?.member_roles?.length > 0) memberId = null; + } + + if (memberId) { + await fetchGraphQL(createMemberRoleMutation, { + member_id: memberId, + team_role: isTeamCreator ? "owner" : "member", + }); + } + + setWorkosSubCacheEntry(externalId, userId, true); + return userId; + } catch (err) { + if (err.status) throw err; + const error = new Error("503: Unable to provision user"); + error.status = 503; + throw error; + } +} + From 25d2f88299f2bd3feb7a634f82cd2bd62ff09f71 Mon Sep 17 00:00:00 2001 From: stefanbaxter Date: Sun, 22 Mar 2026 17:41:33 +0000 Subject: [PATCH 3/7] feat(cubejs): wire FraiOS token support into REST API auth --- services/cubejs/src/utils/checkAuth.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/cubejs/src/utils/checkAuth.js b/services/cubejs/src/utils/checkAuth.js index 326434d0..2929c560 100644 --- a/services/cubejs/src/utils/checkAuth.js +++ b/services/cubejs/src/utils/checkAuth.js @@ -3,8 +3,9 @@ import jwt from "jsonwebtoken"; import { findUser, provisionUserFromWorkOS, + provisionUserFromFraiOS, } from "./dataSourceHelpers.js"; -import { detectTokenType, verifyWorkOSToken } from "./workosAuth.js"; +import { detectTokenType, verifyWorkOSToken, verifyFraiOSToken } from "./workosAuth.js"; import defineUserScope from "./defineUserScope.js"; const { JWT_KEY, JWT_ALGORITHM } = process.env; @@ -50,6 +51,10 @@ const checkAuth = async (req) => { // WorkOS RS256 path const payload = await verifyWorkOSToken(authToken); userId = await provisionUserFromWorkOS(payload); + } else if (tokenType === "fraios") { + // FraiOS HS256 path + const payload = await verifyFraiOSToken(authToken); + userId = await provisionUserFromFraiOS(payload); } else { // Hasura HS256 path (existing) let jwtDecoded; From a11adebc523c94ed0bcff91f538e6f9b534b0e1e Mon Sep 17 00:00:00 2001 From: stefanbaxter Date: Sun, 22 Mar 2026 17:41:58 +0000 Subject: [PATCH 4/7] feat(cubejs): wire FraiOS token support into SQL API auth --- services/cubejs/src/utils/checkSqlAuth.js | 42 ++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/services/cubejs/src/utils/checkSqlAuth.js b/services/cubejs/src/utils/checkSqlAuth.js index d44d4543..31a2a973 100644 --- a/services/cubejs/src/utils/checkSqlAuth.js +++ b/services/cubejs/src/utils/checkSqlAuth.js @@ -2,8 +2,9 @@ import { findSqlCredentials, findUser, provisionUserFromWorkOS, + provisionUserFromFraiOS, } from "./dataSourceHelpers.js"; -import { detectTokenType, verifyWorkOSToken } from "./workosAuth.js"; +import { detectTokenType, verifyWorkOSToken, verifyFraiOSToken } from "./workosAuth.js"; import buildSecurityContext from "./buildSecurityContext.js"; import defineUserScope, { @@ -84,6 +85,45 @@ const checkSqlAuth = async (_, user) => { datasourceId ); + return { + password, + securityContext: { + userId, + userScope, + }, + }; + } else if (tokenType === "fraios") { + // FraiOS JWT path — same flow, different verification + const payload = await verifyFraiOSToken(password); + const userId = await provisionUserFromFraiOS(payload); + + const userData = await findUser({ userId }); + + if (!userData.dataSources?.length || !userData.members?.length) { + const error = new Error(`404: user "${userId}" not found`); + error.status = 404; + throw error; + } + + const datasourceId = username; + const dataSource = userData.dataSources.find( + (ds) => ds.id === datasourceId + ); + + if (!dataSource) { + const error = new Error( + `403: access denied for datasource "${datasourceId}"` + ); + error.status = 403; + throw error; + } + + const userScope = defineUserScope( + userData.dataSources, + userData.members, + datasourceId + ); + return { password, securityContext: { From c19917ab3c4066b7c04173e351c3dfa448a315e9 Mon Sep 17 00:00:00 2001 From: stefanbaxter Date: Sun, 22 Mar 2026 17:42:42 +0000 Subject: [PATCH 5/7] feat(cubejs): accept FraiOS tokens on discover endpoint --- services/cubejs/src/routes/discover.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/services/cubejs/src/routes/discover.js b/services/cubejs/src/routes/discover.js index bc8fdb21..34636a97 100644 --- a/services/cubejs/src/routes/discover.js +++ b/services/cubejs/src/routes/discover.js @@ -1,7 +1,7 @@ import YAML from "yaml"; -import { verifyWorkOSToken } from "../utils/workosAuth.js"; -import { findUser, provisionUserFromWorkOS } from "../utils/dataSourceHelpers.js"; +import { detectTokenType, verifyWorkOSToken, verifyFraiOSToken } from "../utils/workosAuth.js"; +import { findUser, provisionUserFromWorkOS, provisionUserFromFraiOS } from "../utils/dataSourceHelpers.js"; import { parseCubesFromJs } from "../utils/smart-generation/diffModels.js"; /** @@ -82,7 +82,7 @@ export function buildDiscoverResponse(dataSources, partitionTeamIds) { * GET /api/v1/discover * * Returns all datasources and cubes available to the authenticated user. - * Requires a WorkOS Bearer token. The user is auto-provisioned (JIT) if + * Requires a WorkOS or FraiOS Bearer token. The user is auto-provisioned (JIT) if * this is their first request. * * Response: @@ -97,7 +97,7 @@ export function buildDiscoverResponse(dataSources, partitionTeamIds) { */ export default async function discover(req, res) { try { - // --- Auth: WorkOS JWT only --- + // --- Auth: WorkOS or FraiOS JWT --- const authHeader = req.headers.authorization; if (!authHeader) { return res.status(403).json({ error: "Authorization header required" }); @@ -111,8 +111,18 @@ export default async function discover(req, res) { return res.status(403).json({ error: "Bearer token required" }); } - const payload = await verifyWorkOSToken(token); - const userId = await provisionUserFromWorkOS(payload); + const tokenType = detectTokenType(token); + let payload, userId; + + if (tokenType === "workos") { + payload = await verifyWorkOSToken(token); + userId = await provisionUserFromWorkOS(payload); + } else if (tokenType === "fraios") { + payload = await verifyFraiOSToken(token); + userId = await provisionUserFromFraiOS(payload); + } else { + return res.status(403).json({ error: "WorkOS or FraiOS token required" }); + } // --- Fetch user's datasources across all team memberships --- const user = await findUser({ userId }); From f8f7dd111f93ae432e8fc127bd8a692af15bea0a Mon Sep 17 00:00:00 2001 From: stefanbaxter Date: Sun, 22 Mar 2026 17:43:08 +0000 Subject: [PATCH 6/7] feat(cubejs): wire FraiOS token support into Hasura proxy --- services/cubejs/src/routes/hasuraProxy.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/services/cubejs/src/routes/hasuraProxy.js b/services/cubejs/src/routes/hasuraProxy.js index 781f0c30..be4dca5e 100644 --- a/services/cubejs/src/routes/hasuraProxy.js +++ b/services/cubejs/src/routes/hasuraProxy.js @@ -1,8 +1,8 @@ import express from "express"; import { createProxyMiddleware } from "http-proxy-middleware"; -import { detectTokenType, verifyWorkOSToken } from "../utils/workosAuth.js"; -import { provisionUserFromWorkOS } from "../utils/dataSourceHelpers.js"; +import { detectTokenType, verifyWorkOSToken, verifyFraiOSToken } from "../utils/workosAuth.js"; +import { provisionUserFromWorkOS, provisionUserFromFraiOS } from "../utils/dataSourceHelpers.js"; import { mintHasuraToken } from "../utils/mintHasuraToken.js"; import { mintedTokenCache } from "../utils/mintedTokenCache.js"; @@ -74,10 +74,16 @@ export default function createHasuraProxy(config = {}) { const tokenType = detectTokenType(token); - if (tokenType === "workos") { - // RS256 WorkOS token → verify, provision, mint HS256 - const payload = await verifyWorkOSToken(token); - const userId = await provisionUserFromWorkOS(payload); + if (tokenType === "workos" || tokenType === "fraios") { + // WorkOS RS256 or FraiOS HS256 → verify, provision, mint Hasura HS256 + let userId; + if (tokenType === "workos") { + const payload = await verifyWorkOSToken(token); + userId = await provisionUserFromWorkOS(payload); + } else { + const payload = await verifyFraiOSToken(token); + userId = await provisionUserFromFraiOS(payload); + } // Check minted token cache let hasuraToken = mintedTokenCache.get(userId); @@ -94,7 +100,7 @@ export default function createHasuraProxy(config = {}) { // Swap the Authorization header for Hasura req.headers.authorization = `Bearer ${hasuraToken}`; } - // HS256 tokens pass through unchanged + // HS256 Hasura tokens pass through unchanged next(); } catch (err) { From db2def632c69764aa43de8681afa24dca2a40e28 Mon Sep 17 00:00:00 2001 From: stefanbaxter Date: Sun, 22 Mar 2026 17:45:13 +0000 Subject: [PATCH 7/7] chore(cubejs): add TOKEN_SECRET env var for FraiOS auth Plumbs the FraiOS shared secret into the CubeJS service environment. --- .env.example | 3 +++ docker-compose.dev.yml | 1 + services/cubejs/.env.test | 5 ++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 52a2abbd..70d2c439 100644 --- a/.env.example +++ b/.env.example @@ -42,6 +42,9 @@ WORKOS_API_KEY= WORKOS_CLIENT_ID= WORKOS_ISSUER= WORKOS_REDIRECT_URI=http://localhost:3000/auth/callback + +# FraiOS Authentication +TOKEN_SECRET= ALLOWED_RETURN_TO_HOSTS=localhost SIGNUP_ENABLED=false diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 29cc4572..63f30be0 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -65,6 +65,7 @@ services: WORKOS_API_KEY: ${WORKOS_API_KEY} WORKOS_CLIENT_ID: ${WORKOS_CLIENT_ID} WORKOS_ISSUER: ${WORKOS_ISSUER:-} + TOKEN_SECRET: ${TOKEN_SECRET:-} networks: - synmetrix_default diff --git a/services/cubejs/.env.test b/services/cubejs/.env.test index f9c3d742..90baac34 100644 --- a/services/cubejs/.env.test +++ b/services/cubejs/.env.test @@ -11,4 +11,7 @@ HASURA_ENDPOINT=http://localhost:8080/v1/graphql # CubeJS CUBEJS_SECRET=7bbc4032cc82a017434deb9213dac701 -CUBEJS_API_SECRET=7bbc4032cc82a017434deb9213dac701 \ No newline at end of file +CUBEJS_API_SECRET=7bbc4032cc82a017434deb9213dac701 + +# FraiOS +TOKEN_SECRET=test-fraios-secret-key-at-least-32-chars-long \ No newline at end of file