From 6364051995a7e5bfb3df33c5b94dab359d55a643 Mon Sep 17 00:00:00 2001 From: natansil Date: Mon, 20 Jul 2026 21:44:43 +0300 Subject: [PATCH 1/6] Add SSO ID token retrieval --- src/modules/sso.ts | 7 ++++ src/modules/sso.types.ts | 30 +++++++++++++---- tests/unit/sso.test.ts | 71 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 tests/unit/sso.test.ts diff --git a/src/modules/sso.ts b/src/modules/sso.ts index ca6be3bb..c912a13c 100644 --- a/src/modules/sso.ts +++ b/src/modules/sso.ts @@ -20,5 +20,12 @@ export function createSsoModule( const url = `/apps/${appId}/auth/sso/accesstoken/${userid}`; return axios.get(url); }, + + // Get the stored SSO OIDC ID token for a specific user + async getIdToken(userid: string): Promise { + const url = `/apps/${appId}/auth/sso/idtoken/${userid}`; + const response = await axios.get(url); + return response as unknown as string; + }, }; } diff --git a/src/modules/sso.types.ts b/src/modules/sso.types.ts index ecb7459d..0ef697bb 100644 --- a/src/modules/sso.types.ts +++ b/src/modules/sso.types.ts @@ -11,9 +11,9 @@ export interface SsoAccessTokenResponse { /** * SSO (Single Sign-On) module for managing SSO authentication. * - * This module provides methods for retrieving SSO access tokens for users. - * These tokens allow you to authenticate Base44 users with external - * systems or services. + * This module provides methods for retrieving SSO tokens for users. These + * tokens allow you to authenticate Base44 users with external systems or + * services. * * This module is only available to use with a client in service role authentication mode, which means it can only be used in backend environments. * @@ -21,9 +21,15 @@ export interface SsoAccessTokenResponse { * * @example * ```typescript - * // Access SSO module with service role - * const response = await base44.asServiceRole.sso.getAccessToken('user_123'); - * console.log(response.data.access_token); + * import { createClientFromRequest } from 'npm:@base44/sdk'; + * + * Deno.serve(async (req) => { + * const base44 = createClientFromRequest(req); + * const user = await base44.auth.me(); + * const idToken = await base44.asServiceRole.sso.getIdToken(user.id); + * + * return Response.json({ idToken }); + * }); * ``` */ export interface SsoModule { @@ -44,4 +50,16 @@ export interface SsoModule { * ``` */ getAccessToken(userid: string): Promise; + + /** + * Gets the stored SSO OIDC ID token for the current app user. + * + * The service-role client must include an on-behalf-of token for the same + * user specified by `userid`. This method returns the stored token as-is and + * does not refresh it. + * + * @param userid - The current app user's ID. + * @returns Promise resolving to the raw ID-token string. + */ + getIdToken(userid: string): Promise; } diff --git a/tests/unit/sso.test.ts b/tests/unit/sso.test.ts new file mode 100644 index 00000000..0166b3d5 --- /dev/null +++ b/tests/unit/sso.test.ts @@ -0,0 +1,71 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import nock from "nock"; +import { createClient } from "../../src/index.ts"; + +describe("SSO module", () => { + const appId = "test-app-id"; + const serverUrl = "https://base44.app"; + const serviceToken = "service-token-123"; + const userToken = "user-token-456"; + const userId = "user_123"; + let base44: ReturnType; + let scope: nock.Scope; + + beforeEach(() => { + base44 = createClient({ + serverUrl, + appId, + token: userToken, + serviceToken, + }); + scope = nock(serverUrl); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + test("getIdToken issues the app-scoped GET request and returns the raw token", async () => { + const rawIdToken = "header.payload.signature"; + + scope + .get(`/api/apps/${appId}/auth/sso/idtoken/${userId}`) + .reply(200, JSON.stringify(rawIdToken), { + "Content-Type": "application/json", + }); + + const idToken: string = + await base44.asServiceRole.sso.getIdToken(userId); + + expect(idToken).toBe(rawIdToken); + expect(scope.isDone()).toBe(true); + }); + + test("getAccessToken keeps its existing request and return behavior", async () => { + const accessTokenResponse = { access_token: "access-token-123" }; + + scope + .get(`/api/apps/${appId}/auth/sso/accesstoken/${userId}`) + .reply(200, accessTokenResponse); + + const response = + await base44.asServiceRole.sso.getAccessToken(userId); + + expect(response).toEqual(accessTokenResponse); + expect(scope.isDone()).toBe(true); + }); + + test("getIdToken uses the service-role client with on-behalf-of authentication", async () => { + scope + .get(`/api/apps/${appId}/auth/sso/idtoken/${userId}`) + .matchHeader("Authorization", `Bearer ${serviceToken}`) + .matchHeader("on-behalf-of", `Bearer ${userToken}`) + .reply(200, JSON.stringify("raw-id-token"), { + "Content-Type": "application/json", + }); + + await base44.asServiceRole.sso.getIdToken(userId); + + expect(scope.isDone()).toBe(true); + }); +}); From a375fed53e32649bc125a3387a0f3ecf0dcc175a Mon Sep 17 00:00:00 2001 From: natansil Date: Tue, 21 Jul 2026 12:06:58 +0300 Subject: [PATCH 2/6] Preserve SSO access token example --- src/modules/sso.types.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/modules/sso.types.ts b/src/modules/sso.types.ts index 0ef697bb..444c93e7 100644 --- a/src/modules/sso.types.ts +++ b/src/modules/sso.types.ts @@ -21,15 +21,9 @@ export interface SsoAccessTokenResponse { * * @example * ```typescript - * import { createClientFromRequest } from 'npm:@base44/sdk'; - * - * Deno.serve(async (req) => { - * const base44 = createClientFromRequest(req); - * const user = await base44.auth.me(); - * const idToken = await base44.asServiceRole.sso.getIdToken(user.id); - * - * return Response.json({ idToken }); - * }); + * // Access SSO module with service role + * const response = await base44.asServiceRole.sso.getAccessToken('user_123'); + * console.log(response.data.access_token); * ``` */ export interface SsoModule { @@ -60,6 +54,19 @@ export interface SsoModule { * * @param userid - The current app user's ID. * @returns Promise resolving to the raw ID-token string. + * + * @example + * ```typescript + * import { createClientFromRequest } from 'npm:@base44/sdk'; + * + * Deno.serve(async (req) => { + * const base44 = createClientFromRequest(req); + * const user = await base44.auth.me(); + * const idToken = await base44.asServiceRole.sso.getIdToken(user.id); + * + * return Response.json({ idToken }); + * }); + * ``` */ getIdToken(userid: string): Promise; } From 4dbbf56f19859fba3fd237d83c9148635978317c Mon Sep 17 00:00:00 2001 From: natansil Date: Tue, 21 Jul 2026 13:23:56 +0300 Subject: [PATCH 3/6] Limit SSO tests to ID tokens --- tests/unit/sso.test.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/unit/sso.test.ts b/tests/unit/sso.test.ts index 0166b3d5..fbac3ed2 100644 --- a/tests/unit/sso.test.ts +++ b/tests/unit/sso.test.ts @@ -41,20 +41,6 @@ describe("SSO module", () => { expect(scope.isDone()).toBe(true); }); - test("getAccessToken keeps its existing request and return behavior", async () => { - const accessTokenResponse = { access_token: "access-token-123" }; - - scope - .get(`/api/apps/${appId}/auth/sso/accesstoken/${userId}`) - .reply(200, accessTokenResponse); - - const response = - await base44.asServiceRole.sso.getAccessToken(userId); - - expect(response).toEqual(accessTokenResponse); - expect(scope.isDone()).toBe(true); - }); - test("getIdToken uses the service-role client with on-behalf-of authentication", async () => { scope .get(`/api/apps/${appId}/auth/sso/idtoken/${userId}`) From 4156dac85af4224e63fc7431f661ba7ebb9b1cda Mon Sep 17 00:00:00 2001 From: natansil Date: Tue, 21 Jul 2026 13:26:36 +0300 Subject: [PATCH 4/6] Restore SSO access token coverage --- tests/unit/sso.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/unit/sso.test.ts b/tests/unit/sso.test.ts index fbac3ed2..332e6ee2 100644 --- a/tests/unit/sso.test.ts +++ b/tests/unit/sso.test.ts @@ -41,6 +41,22 @@ describe("SSO module", () => { expect(scope.isDone()).toBe(true); }); + test("getAccessToken issues the existing GET request and returns the raw token", async () => { + const rawAccessToken = "access-token-123"; + + scope + .get(`/api/apps/${appId}/auth/sso/accesstoken/${userId}`) + .reply(200, JSON.stringify(rawAccessToken), { + "Content-Type": "application/json", + }); + + const accessToken = + await base44.asServiceRole.sso.getAccessToken(userId); + + expect(accessToken).toBe(rawAccessToken); + expect(scope.isDone()).toBe(true); + }); + test("getIdToken uses the service-role client with on-behalf-of authentication", async () => { scope .get(`/api/apps/${appId}/auth/sso/idtoken/${userId}`) From 39c5538fb5e4a70a436822f0da854e7258f3fdd1 Mon Sep 17 00:00:00 2001 From: natansil Date: Tue, 21 Jul 2026 15:50:33 +0300 Subject: [PATCH 5/6] Address SSO review feedback --- src/modules/sso.ts | 3 ++- src/modules/sso.types.ts | 2 -- tests/unit/sso.test.ts | 25 +++++++++++++++++++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/modules/sso.ts b/src/modules/sso.ts index c912a13c..145982ce 100644 --- a/src/modules/sso.ts +++ b/src/modules/sso.ts @@ -6,7 +6,6 @@ import { SsoModule } from "./sso.types"; * * @param axios - Axios instance * @param appId - Application ID - * @param userToken - User authentication token * @returns SSO module with authentication methods * @internal */ @@ -25,6 +24,8 @@ export function createSsoModule( async getIdToken(userid: string): Promise { const url = `/apps/${appId}/auth/sso/idtoken/${userid}`; const response = await axios.get(url); + // The configured response interceptor unwraps response.data at runtime, + // while AxiosInstance retains AxiosResponse typings. return response as unknown as string; }, }; diff --git a/src/modules/sso.types.ts b/src/modules/sso.types.ts index 444c93e7..023bfb4e 100644 --- a/src/modules/sso.types.ts +++ b/src/modules/sso.types.ts @@ -1,5 +1,3 @@ -import { AxiosResponse } from "axios"; - /** * Response from SSO access token endpoint. * @internal diff --git a/tests/unit/sso.test.ts b/tests/unit/sso.test.ts index 332e6ee2..04fe2c50 100644 --- a/tests/unit/sso.test.ts +++ b/tests/unit/sso.test.ts @@ -53,20 +53,41 @@ describe("SSO module", () => { const accessToken = await base44.asServiceRole.sso.getAccessToken(userId); + // Preserve the legacy public response type for compatibility while + // locking down the endpoint's existing raw-string runtime behavior. expect(accessToken).toBe(rawAccessToken); expect(scope.isDone()).toBe(true); }); test("getIdToken uses the service-role client with on-behalf-of authentication", async () => { + const rawIdToken = "raw-id-token"; + scope .get(`/api/apps/${appId}/auth/sso/idtoken/${userId}`) .matchHeader("Authorization", `Bearer ${serviceToken}`) .matchHeader("on-behalf-of", `Bearer ${userToken}`) - .reply(200, JSON.stringify("raw-id-token"), { + .reply(200, JSON.stringify(rawIdToken), { "Content-Type": "application/json", }); - await base44.asServiceRole.sso.getIdToken(userId); + const idToken = await base44.asServiceRole.sso.getIdToken(userId); + + expect(idToken).toBe(rawIdToken); + expect(scope.isDone()).toBe(true); + }); + + test("getIdToken surfaces a 404 when no ID token is stored", async () => { + scope + .get(`/api/apps/${appId}/auth/sso/idtoken/${userId}`) + .reply(404, { detail: "No ID token stored", code: "NOT_FOUND" }); + + await expect( + base44.asServiceRole.sso.getIdToken(userId) + ).rejects.toMatchObject({ + name: "Base44Error", + status: 404, + code: "NOT_FOUND", + }); expect(scope.isDone()).toBe(true); }); From 0ca9ff931275d083fefb93013a3b3abedff8d169 Mon Sep 17 00:00:00 2001 From: natansil Date: Tue, 21 Jul 2026 16:13:05 +0300 Subject: [PATCH 6/6] Mirror access token retrieval for ID tokens --- src/modules/sso.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/modules/sso.ts b/src/modules/sso.ts index 145982ce..c947878d 100644 --- a/src/modules/sso.ts +++ b/src/modules/sso.ts @@ -21,12 +21,9 @@ export function createSsoModule( }, // Get the stored SSO OIDC ID token for a specific user - async getIdToken(userid: string): Promise { + async getIdToken(userid: string) { const url = `/apps/${appId}/auth/sso/idtoken/${userid}`; - const response = await axios.get(url); - // The configured response interceptor unwraps response.data at runtime, - // while AxiosInstance retains AxiosResponse typings. - return response as unknown as string; + return axios.get(url); }, }; }