diff --git a/package.json b/package.json index 2336d985..35516eaf 100644 --- a/package.json +++ b/package.json @@ -54,9 +54,9 @@ "lint": "prettier -c --parser typescript \"{src,__tests__,e2e}/**/*.[jt]s?(x)\"", "lint:fix": "pnpm run lint --write", "test:types": "tsc --build tsconfig.json", - "test:unit": "vitest --coverage", + "test:unit": "cross-env GOOGLE_CLOUD_PROJECT='vue-fire-store' vitest --coverage", "firebase:emulators": "firebase emulators:start", - "test:dev": "vitest", + "test:dev": "cross-env GOOGLE_CLOUD_PROJECT='vue-fire-store' vitest", "test": "pnpm run lint && pnpm run test:types && pnpm run build && pnpm run -C packages/nuxt build && pnpm run test:unit run", "prepare": "simple-git-hooks" }, @@ -71,6 +71,7 @@ ], "license": "MIT", "dependencies": { + "jwt-decode": "^4.0.0", "vue-demi": "latest" }, "peerDependencies": { @@ -96,6 +97,7 @@ "chalk": "^5.3.0", "consola": "^3.2.3", "conventional-changelog-cli": "^2.0.34", + "cross-env": "^7.0.3", "enquirer": "^2.4.1", "execa": "^8.0.1", "firebase": "^10.8.0", diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index cf8e1b9d..24590538 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -32,8 +32,8 @@ "build": "nuxt-module-build build", "lint": "eslint src", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . -l nuxt-vuefire -r 1", - "test": "vitest", - "dev": "nuxi dev playground", + "test": "cross-env GOOGLE_CLOUD_PROJECT='vue-fire-store' vitest", + "dev": "cross-env GOOGLE_CLOUD_PROJECT='vue-fire-store' nuxi dev playground", "dev:build": "nuxi build playground", "dev:prepare": "nuxt-module-build --stub" }, @@ -71,6 +71,7 @@ "firebase-admin": "^12.0.0", "firebase-functions": "^4.7.0", "nuxt": "^3.10.3", + "playwright-core": "^1.43.1", "vuefire": "workspace:*" } } diff --git a/packages/nuxt/playground/components/ServerOnlyPre.vue b/packages/nuxt/playground/components/ServerOnlyPre.vue new file mode 100644 index 00000000..c2c3a31f --- /dev/null +++ b/packages/nuxt/playground/components/ServerOnlyPre.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/nuxt/playground/pages/authentication.vue b/packages/nuxt/playground/pages/authentication.vue index f21ce0ec..b622e553 100644 --- a/packages/nuxt/playground/pages/authentication.vue +++ b/packages/nuxt/playground/pages/authentication.vue @@ -45,7 +45,9 @@ watch(user, (user) => { // new user const email = ref('') const password = ref('') -function signUp() { +const tenant = ref(null) + +async function signUp() { // link to an existing anonymous account if (user.value?.isAnonymous) { credential = EmailAuthProvider.credential(email.value, password.value) @@ -59,7 +61,7 @@ function signUp() { return createUserWithEmailAndPassword(auth, email.value, password.value) } -function signinPopup() { +async function signinPopup() { return signInWithPopup(auth, googleAuthProvider).then((result) => { const googleCredential = GoogleAuthProvider.credentialFromResult(result) credential = googleCredential @@ -96,37 +98,42 @@ onMounted(() => { diff --git a/packages/nuxt/src/runtime/auth/api.session-verification.ts b/packages/nuxt/src/runtime/auth/api.session-verification.ts index 6de46341..6cb3317a 100644 --- a/packages/nuxt/src/runtime/auth/api.session-verification.ts +++ b/packages/nuxt/src/runtime/auth/api.session-verification.ts @@ -10,6 +10,7 @@ import { import { ensureAdminApp } from 'vuefire/server' import { logger } from '../logging' import { useRuntimeConfig } from '#imports' +import { parseTenantFromFirebaseJwt } from 'vuefire' /** * Setups an API endpoint to be used by the client to mint a cookie based auth session. @@ -27,7 +28,12 @@ export default defineEventHandler(async (event) => { }, 'session-verification' ) - const adminAuth = getAdminAuth(adminApp) + + const tenant = parseTenantFromFirebaseJwt(token) + + const adminAuth = tenant + ? getAdminAuth(adminApp).tenantManager().authForTenant(tenant) + : getAdminAuth(adminApp) logger.debug(token ? 'Verifying the token' : 'Deleting the session cookie') const verifiedIdToken = token ? await adminAuth.verifyIdToken(token) : null diff --git a/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts b/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts index 5f7fdac6..4cf42d59 100644 --- a/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts +++ b/packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts @@ -17,7 +17,6 @@ export default defineNuxtPlugin(async (nuxtApp) => { const event = useRequestEvent() const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp const firebaseAdminApp = nuxtApp.$firebaseAdminApp as AdminApp - const adminAuth = getAdminAuth(firebaseAdminApp) const auth = nuxtApp.$firebaseAuth as Auth const decodedToken = nuxtApp[ @@ -27,10 +26,17 @@ export default defineNuxtPlugin(async (nuxtApp) => { const uid = decodedToken?.uid + const tenant = decodedToken?.firebase?.tenant + + const adminAuth = tenant + ? getAdminAuth(firebaseAdminApp).tenantManager().authForTenant(tenant) + : getAdminAuth(firebaseAdminApp) + // this is also undefined if the user hasn't enabled the session cookie option if (uid) { // reauthenticate if the user is not the same (e.g. invalidated) - if (auth.currentUser?.uid !== uid) { + // OR multi tenancy is used, otherwise tenantId won't be present in SSR accessToken + if (auth.currentUser?.uid !== uid || tenant) { const customToken = await adminAuth .createCustomToken(uid) .catch((err) => { @@ -40,6 +46,8 @@ export default defineNuxtPlugin(async (nuxtApp) => { // console.timeLog('token', `got token for ${user.uid}`) if (customToken) { logger.debug('Signing in with custom token') + // Update firebase/auth tenantId to ensure it is set during SSR + auth.tenantId = tenant ?? null // TODO: allow user to handle error? await signInWithCustomToken(auth, customToken) // console.timeLog('token', `signed in with token for ${user.uid}`) diff --git a/packages/nuxt/src/runtime/auth/plugin-user-token.server.ts b/packages/nuxt/src/runtime/auth/plugin-user-token.server.ts index 5d8d56fd..07f602b1 100644 --- a/packages/nuxt/src/runtime/auth/plugin-user-token.server.ts +++ b/packages/nuxt/src/runtime/auth/plugin-user-token.server.ts @@ -12,7 +12,7 @@ export default defineNuxtPlugin(async (nuxtApp) => { const adminApp = nuxtApp.$firebaseAdminApp as AdminApp const decodedToken = await decodeSessionCookie( - getCookie(event, AUTH_COOKIE_NAME), + event && getCookie(event, AUTH_COOKIE_NAME), adminApp ) diff --git a/packages/nuxt/tests/auth.spec.ts b/packages/nuxt/tests/auth.spec.ts new file mode 100644 index 00000000..70e227f1 --- /dev/null +++ b/packages/nuxt/tests/auth.spec.ts @@ -0,0 +1,138 @@ +import { describe, expect, it } from 'vitest' +import { createPage, setup } from '@nuxt/test-utils/e2e' +import { createResolver } from '@nuxt/kit' + +const { resolve } = createResolver(import.meta.url) + +await setup({ + rootDir: resolve('../playground'), + build: false, + server: true, + browser: true, + dev: true, + + browserOptions: { + type: 'chromium', + launch: { + headless: true, + }, + }, +}) + +describe('auth/multi-tenancy', async () => { + it('should create a default tenant token if no tenant is specified', async () => { + const page = await createPage('/authentication') + + // 1. Sign out, clear tenant to start clean + await page.getByTestId('sign-out').click() + await page.getByTestId('tenant').clear() + + // 2. Ensure test account exists + const signupResponse = page.waitForResponse((r) => + r.url().includes('accounts:signUp') + ) + await page.getByTestId('email-signup').fill('test@test.com') + await page.getByTestId('password-signup').fill('testtest') + await page.getByTestId('submit-signup').click() + await signupResponse + + // 3. Log in with test account, check tenant + // Call to sign in is 'accounts:signInWithPassword', but we need __session call to get user info + const signinResponse = page.waitForResponse((r) => + r.url().includes('/api/__session') + ) + await page.getByTestId('email-signin').fill('test@test.com') + await page.getByTestId('password-signin').fill('testtest') + await page.getByTestId('submit-signin').click() + await signinResponse + + // 4. Assert user does in fact not have a tenant id + const userData = await page.getByTestId('user-data-client').textContent() + + expect(userData).toBeTruthy() + if (!userData) return + + const user = JSON.parse(userData) + expect(user.tenantId).toBeUndefined() + }) + + it('should create token with tenantId if tenant name is specified', async () => { + const page = await createPage('/authentication') + const tenantName = 'tenant A' + + // 1. Sign out, clear tenant to start clean + await page.getByTestId('sign-out').click() + await page.getByTestId('tenant').clear() + await page.getByTestId('tenant').fill(tenantName) + + // 2. Ensure test account exists + const signupResponse = page.waitForResponse((r) => + r.url().includes('accounts:signUp') + ) + await page.getByTestId('email-signup').fill('test@test.com') + await page.getByTestId('password-signup').fill('testtest') + await page.getByTestId('submit-signup').click() + await signupResponse + + // 3. Log in with test account, check tenant + // Call to sign in is 'accounts:signInWithPassword', but we need __session call to get user info + const signinResponse = page.waitForResponse((r) => + r.url().includes('/api/__session') + ) + await page.getByTestId('email-signin').fill('test@test.com') + await page.getByTestId('password-signin').fill('testtest') + await page.getByTestId('submit-signin').click() + await signinResponse + + // 4. Assert user does in fact not have a tenant id + const userData = await page.getByTestId('user-data-client').textContent() + + expect(userData).toBeTruthy() + if (!userData) return + + const user = JSON.parse(userData) + expect(user.tenantId).toEqual(tenantName) + }) + + it('should return tenantId in server render', async () => { + const page = await createPage('/authentication') + const tenantName = 'tenant A' + + // 1. Sign out, clear tenant to start clean + await page.getByTestId('sign-out').click() + await page.getByTestId('tenant').clear() + await page.getByTestId('tenant').fill(tenantName) + + // 2. Ensure test account exists + const signupResponse = page.waitForResponse((r) => + r.url().includes('accounts:signUp') + ) + await page.getByTestId('email-signup').fill('test@test.com') + await page.getByTestId('password-signup').fill('testtest') + await page.getByTestId('submit-signup').click() + await signupResponse + + // 3. Log in with test account, check tenant + // Call to sign in is 'accounts:signInWithPassword', but we need __session call to get user info + const signinResponse = page.waitForResponse((r) => + r.url().includes('/api/__session') + ) + await page.getByTestId('email-signin').fill('test@test.com') + await page.getByTestId('password-signin').fill('testtest') + await page.getByTestId('submit-signin').click() + await signinResponse + + // 4. Reload the page to trigger server render + await page.reload({ waitUntil: 'domcontentloaded' }) + + const serverUserData = await page + .getByTestId('user-data-server') + .textContent() + + expect(serverUserData).toBeTruthy() + if (!serverUserData) return + + const serverUser = JSON.parse(serverUserData) + expect(serverUser.tenantId).toEqual(tenantName) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 192a5a7a..f03a8323 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + jwt-decode: + specifier: ^4.0.0 + version: 4.0.0 vue-demi: specifier: latest version: 0.14.7(vue@3.4.19) @@ -30,6 +33,9 @@ importers: conventional-changelog-cli: specifier: ^2.0.34 version: 2.2.2 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 enquirer: specifier: ^2.4.1 version: 2.4.1 @@ -83,7 +89,7 @@ importers: version: 2.0.0(typescript@5.3.3) vitepress: specifier: 1.0.0-rc.44 - version: 1.0.0-rc.44(@algolia/client-search@4.22.1)(search-insights@2.13.0)(typescript@5.3.3) + version: 1.0.0-rc.44(@algolia/client-search@4.22.1)(jwt-decode@4.0.0)(search-insights@2.13.0)(typescript@5.3.3) vitest: specifier: ^1.3.1 version: 1.3.1(happy-dom@13.4.1) @@ -117,7 +123,7 @@ importers: version: 3.10.3(rollup@3.29.4) '@nuxt/test-utils': specifier: ^3.11.0 - version: 3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) + version: 3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(playwright-core@1.43.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) eslint: specifier: ^8.56.0 version: 8.56.0 @@ -133,6 +139,9 @@ importers: nuxt: specifier: ^3.10.3 version: 3.10.3(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.1.4) + playwright-core: + specifier: ^1.43.1 + version: 1.43.1 vuefire: specifier: workspace:* version: link:../.. @@ -2312,7 +2321,7 @@ packages: - supports-color dev: true - /@nuxt/test-utils@3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19): + /@nuxt/test-utils@3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(playwright-core@1.43.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19): resolution: {integrity: sha512-9ovgpQZkZpVg/MhYVVn2169WjH/IL0XUqwGryTa/lkx0/BCi1LMVEp3HTPkmt4qbRcxitO+kL4vFqqrFGVaSVg==} engines: {node: ^14.18.0 || >=16.10.0} peerDependencies: @@ -2368,6 +2377,7 @@ packages: ofetch: 1.3.3 pathe: 1.1.2 perfect-debounce: 1.0.0 + playwright-core: 1.43.1 radix3: 1.1.0 scule: 1.3.0 std-env: 3.7.0 @@ -2376,7 +2386,7 @@ packages: unplugin: 1.7.1 vite: 5.1.4(@types/node@20.11.20) vitest: 1.3.1(happy-dom@13.4.1) - vitest-environment-nuxt: 1.0.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) + vitest-environment-nuxt: 1.0.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(playwright-core@1.43.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) vue: 3.4.19(typescript@5.3.3) vue-router: 4.3.0(vue@3.4.19) transitivePeerDependencies: @@ -3775,7 +3785,7 @@ packages: - vue dev: true - /@vueuse/integrations@10.8.0(focus-trap@7.5.4)(vue@3.4.19): + /@vueuse/integrations@10.8.0(focus-trap@7.5.4)(jwt-decode@4.0.0)(vue@3.4.19): resolution: {integrity: sha512-sw3P/7cXOfNLQfERp7P0IJ2ODjLE2C3BGXpBQJQkS309c1jbJak9yu4EnY70WaZjkj53aeWSFU6BbHrUxXJ7SA==} peerDependencies: async-validator: '*' @@ -3819,6 +3829,7 @@ packages: '@vueuse/core': 10.8.0(vue@3.4.19) '@vueuse/shared': 10.8.0(vue@3.4.19) focus-trap: 7.5.4 + jwt-decode: 4.0.0 vue-demi: 0.14.7(vue@3.4.19) transitivePeerDependencies: - '@vue/composition-api' @@ -4887,6 +4898,14 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true + /cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + dependencies: + cross-spawn: 7.0.3 + dev: true + /cross-spawn@6.0.5: resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} engines: {node: '>=4.8'} @@ -7279,6 +7298,10 @@ packages: dev: true optional: true + /jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -8773,6 +8796,12 @@ packages: mlly: 1.6.0 pathe: 1.1.2 + /playwright-core@1.43.1: + resolution: {integrity: sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==} + engines: {node: '>=16'} + hasBin: true + dev: true + /postcss-calc@9.0.1(postcss@8.4.35): resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==} engines: {node: ^14 || ^16 || >=18.0} @@ -10965,7 +10994,7 @@ packages: fsevents: 2.3.3 dev: true - /vitepress@1.0.0-rc.44(@algolia/client-search@4.22.1)(search-insights@2.13.0)(typescript@5.3.3): + /vitepress@1.0.0-rc.44(@algolia/client-search@4.22.1)(jwt-decode@4.0.0)(search-insights@2.13.0)(typescript@5.3.3): resolution: {integrity: sha512-tO5taxGI7fSpBK1D8zrZTyJJERlyU9nnt0jHSt3fywfq3VKn977Hg0wUuTkEmwXlFYwuW26+6+3xorf4nD3XvA==} hasBin: true peerDependencies: @@ -10985,7 +11014,7 @@ packages: '@vitejs/plugin-vue': 5.0.4(vite@5.1.4)(vue@3.4.19) '@vue/devtools-api': 7.0.15(vue@3.4.19) '@vueuse/core': 10.8.0(vue@3.4.19) - '@vueuse/integrations': 10.8.0(focus-trap@7.5.4)(vue@3.4.19) + '@vueuse/integrations': 10.8.0(focus-trap@7.5.4)(jwt-decode@4.0.0)(vue@3.4.19) focus-trap: 7.5.4 mark.js: 8.11.1 minisearch: 6.3.0 @@ -11020,10 +11049,10 @@ packages: - universal-cookie dev: true - /vitest-environment-nuxt@1.0.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19): + /vitest-environment-nuxt@1.0.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(playwright-core@1.43.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19): resolution: {integrity: sha512-AWMO9h4HdbaFdPWZw34gALFI8gbBiOpvfbyeZwHIPfh4kWg/TwElYHvYMQ61WPUlCGaS5LebfHkaI0WPyb//Iw==} dependencies: - '@nuxt/test-utils': 3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) + '@nuxt/test-utils': 3.11.0(@vue/test-utils@2.4.4)(h3@1.10.2)(happy-dom@13.4.1)(playwright-core@1.43.1)(rollup@3.29.4)(vite@5.1.4)(vitest@1.3.1)(vue-router@4.3.0)(vue@3.4.19) transitivePeerDependencies: - '@cucumber/cucumber' - '@jest/globals' diff --git a/src/auth/index.ts b/src/auth/index.ts index bac5b1b8..68bbe2f0 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -14,6 +14,8 @@ import { getGlobalScope } from '../globals' import { isClient, _Nullable } from '../shared' import { authUserMap, setupOnAuthStateChanged } from './user' import { type VueFireModule } from '..' +import { jwtDecode } from 'jwt-decode' +import type { DecodedIdToken } from 'firebase-admin/auth' export { useCurrentUser, @@ -181,3 +183,25 @@ export function useFirebaseAuth(name?: string) { } return isClient ? inject(_VueFireAuthKey) : null } + +export function parseTenantFromFirebaseJwt( + jwt?: string | undefined | null +): string | null { + if (!jwt) return null + + try { + const decodedToken = jwtDecode(jwt) + if (!decodedToken) return null + + const firebase = decodedToken.firebase + if (!firebase) return null + + return firebase.tenant ?? null + } catch (error) { + if (process.env.NODE_ENV !== 'production') { + console.warn('[VueFire] could not parse tenant from jwt: ', error) + } + } + + return null +} diff --git a/src/index.ts b/src/index.ts index abf9c7bc..68249df1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -96,6 +96,7 @@ export { _VueFireAuthKey, getCurrentUser, updateCurrentUserProfile, + parseTenantFromFirebaseJwt, } from './auth' /** diff --git a/src/server/auth.ts b/src/server/auth.ts index 650378fd..1164a39b 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -9,7 +9,7 @@ import type { App as AdminApp } from 'firebase-admin/app' import { getAuth as getAdminAuth } from 'firebase-admin/auth' import { logger } from './logging' import { isFirebaseError } from './utils' -import { _VueFireAuthKey } from '../auth' +import { _VueFireAuthKey, parseTenantFromFirebaseJwt } from '../auth' // MUST be named `__session` to be kept in Firebase context, therefore this name is hardcoded // https://firebase.google.com/docs/hosting/manage-cache#using_cookies @@ -112,9 +112,13 @@ export async function decodeSessionCookie( adminApp: AdminApp ): Promise { if (sessionCookie) { - const adminAuth = getAdminAuth(adminApp) - try { + const tenant = parseTenantFromFirebaseJwt(sessionCookie) + + const adminAuth = tenant + ? getAdminAuth(adminApp).tenantManager().authForTenant(tenant) + : getAdminAuth(adminApp) + // TODO: should we check for the revoked status of the token here? // we await to try/catch // return await adminAuth.verifyIdToken(token /*, checkRevoked */) diff --git a/tests/auth/parse-tenants-from-token.spec.ts b/tests/auth/parse-tenants-from-token.spec.ts new file mode 100644 index 00000000..a9256ad6 --- /dev/null +++ b/tests/auth/parse-tenants-from-token.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' +import { parseTenantFromFirebaseJwt } from '../../src' + +describe('auth/parseTenantFromFirebaseJwt', () => { + it('should return null if supplied jwt is null', () => { + const sut = null + + const result = parseTenantFromFirebaseJwt(sut) + + expect(result).toBeNull() + }) + + it('should return null if supplied jwt is an empty string', () => { + const sut = '' + + const result = parseTenantFromFirebaseJwt(sut) + + expect(result).toBeNull() + }) + + it('should return null if supplied jwt is an invalid jwt', () => { + const sut = 'a.b.c' + + const result = parseTenantFromFirebaseJwt(sut) + + expect(result).toBeNull() + }) + + it('should return null if supplied jwt does not have the right structure', () => { + // { + // "tenant": "test" + // } + const sut = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0ZW5hbnQiOiJ0ZXN0In0' + + const result = parseTenantFromFirebaseJwt(sut) + + expect(result).toBeNull() + }) + + it('should return payload tenant if supplied jwt is valid but has no signature', () => { + // { + // "firebase": { + // "tenant": "test" + // } + // } + const sut = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJmaXJlYmFzZSI6eyJ0ZW5hbnQiOiJ0ZXN0In19' + + const result = parseTenantFromFirebaseJwt(sut) + + expect(result).toEqual('test') + }) +})