Skip to content

Commit d7994d1

Browse files
authored
clear http only cookies on logout (#91)
1 parent 3717a70 commit d7994d1

5 files changed

Lines changed: 127 additions & 51 deletions

File tree

src/client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ export function createClient(config: CreateClientConfig): Base44Client {
7070
headers: optionalHeaders,
7171
} = config;
7272

73+
// Normalize appBaseUrl to always be a string (empty if not provided or invalid)
74+
const normalizedAppBaseUrl = typeof appBaseUrl === "string" ? appBaseUrl : "";
75+
7376
const socketConfig: RoomsSocketConfig = {
7477
serverUrl,
7578
mountPath: "/ws-user-apps/socket.io/",
@@ -135,7 +138,7 @@ export function createClient(config: CreateClientConfig): Base44Client {
135138
functionsAxiosClient,
136139
appId,
137140
{
138-
appBaseUrl,
141+
appBaseUrl: normalizedAppBaseUrl,
139142
serverUrl,
140143
}
141144
);

src/modules/auth.ts

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ export function createAuthModule(
4949
: window.location.href;
5050

5151
// Build the login URL
52-
const loginUrl = `${
53-
options.appBaseUrl ?? ""
54-
}/login?from_url=${encodeURIComponent(redirectUrl)}`;
52+
const loginUrl = `${options.appBaseUrl}/login?from_url=${encodeURIComponent(redirectUrl)}`;
5553

5654
// Redirect to the login page
5755
window.location.href = loginUrl;
@@ -65,7 +63,7 @@ export function createAuthModule(
6563
// Build the provider login URL (google is the default, so no provider path needed)
6664
const providerPath = provider === "google" ? "" : `/${provider}`;
6765
const loginUrl = `${
68-
options.serverUrl
66+
options.appBaseUrl
6967
}/api/apps/auth${providerPath}/login?app_id=${appId}&from_url=${encodeURIComponent(
7068
redirectUrl
7169
)}`;
@@ -75,29 +73,29 @@ export function createAuthModule(
7573
},
7674

7775
// Logout the current user
78-
// Removes the token from localStorage and optionally redirects to a URL or reloads the page
7976
logout(redirectUrl?: string) {
80-
// Remove token from axios headers
77+
// Remove token from axios headers (always do this)
8178
delete axios.defaults.headers.common["Authorization"];
8279

83-
// Remove token from localStorage
84-
if (typeof window !== "undefined" && window.localStorage) {
85-
try {
86-
window.localStorage.removeItem("base44_access_token");
87-
// Remove "token" that is set by the built-in SDK of platform version 2
88-
window.localStorage.removeItem("token");
89-
} catch (e) {
90-
console.error("Failed to remove token from localStorage:", e);
91-
}
92-
}
93-
94-
// Redirect if a URL is provided
80+
// Only do the rest if in a browser environment
9581
if (typeof window !== "undefined") {
96-
if (redirectUrl) {
97-
window.location.href = redirectUrl;
98-
} else {
99-
window.location.reload();
82+
// Remove token from localStorage
83+
if (window.localStorage) {
84+
try {
85+
window.localStorage.removeItem("base44_access_token");
86+
// Remove "token" that is set by the built-in SDK of platform version 2
87+
window.localStorage.removeItem("token");
88+
} catch (e) {
89+
console.error("Failed to remove token from localStorage:", e);
90+
}
10091
}
92+
93+
// Determine the from_url parameter
94+
const fromUrl = redirectUrl || window.location.href;
95+
96+
// Redirect to server-side logout endpoint to clear HTTP-only cookies
97+
const logoutUrl = `${options.appBaseUrl}/api/apps/auth/logout?from_url=${encodeURIComponent(fromUrl)}`;
98+
window.location.href = logoutUrl;
10199
}
102100
},
103101

src/modules/auth.types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ export interface ResetPasswordParams {
9898
export interface AuthModuleOptions {
9999
/** Server URL for API requests. */
100100
serverUrl: string;
101-
/** Optional base URL for the app (used for login redirects). */
102-
appBaseUrl?: string;
101+
/** Base URL for the app (used for login redirects). */
102+
appBaseUrl: string;
103103
}
104104

105105
/**

tests/unit/auth.test.js

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,33 @@ describe('Auth Module', () => {
77
let scope;
88
const appId = 'test-app-id';
99
const serverUrl = 'https://api.base44.com';
10-
10+
const appBaseUrl = 'https://api.base44.com';
11+
1112
beforeEach(() => {
13+
// Mock window.addEventListener and document for analytics module
14+
if (typeof window !== 'undefined') {
15+
if (!window.addEventListener) {
16+
window.addEventListener = vi.fn();
17+
window.removeEventListener = vi.fn();
18+
}
19+
}
20+
if (typeof document === 'undefined') {
21+
global.document = {
22+
referrer: '',
23+
visibilityState: 'visible'
24+
};
25+
}
26+
1227
// Create a new client for each test
1328
base44 = createClient({
1429
serverUrl,
1530
appId,
31+
appBaseUrl,
1632
});
17-
33+
1834
// Create a nock scope for mocking API calls
1935
scope = nock(serverUrl);
20-
36+
2137
// Enable request debugging for Nock
2238
nock.disableNetConnect();
2339
nock.emitter.on('no match', (req) => {
@@ -143,15 +159,15 @@ describe('Auth Module', () => {
143159
global.window = {
144160
location: mockLocation
145161
};
146-
162+
147163
const nextUrl = 'https://example.com/dashboard';
148164
base44.auth.redirectToLogin(nextUrl);
149-
165+
150166
// Verify the redirect URL was set correctly
151167
expect(mockLocation.href).toBe(
152-
`/login?from_url=${encodeURIComponent(nextUrl)}`
168+
`${appBaseUrl}/login?from_url=${encodeURIComponent(nextUrl)}`
153169
);
154-
170+
155171
// Restore window
156172
global.window = originalWindow;
157173
});
@@ -169,7 +185,7 @@ describe('Auth Module', () => {
169185

170186
// Verify the redirect URL uses current URL
171187
expect(mockLocation.href).toBe(
172-
`/login?from_url=${encodeURIComponent(currentUrl)}`
188+
`${appBaseUrl}/login?from_url=${encodeURIComponent(currentUrl)}`
173189
);
174190

175191
// Restore window
@@ -204,6 +220,12 @@ describe('Auth Module', () => {
204220
});
205221

206222
test('should use relative URL for login redirect when appBaseUrl is not provided', () => {
223+
// Create a client without appBaseUrl
224+
const clientWithoutAppBaseUrl = createClient({
225+
serverUrl,
226+
appId,
227+
});
228+
207229
// Mock window.location
208230
const originalWindow = global.window;
209231
const mockLocation = { href: '', origin: 'https://current-app.com' };
@@ -212,7 +234,7 @@ describe('Auth Module', () => {
212234
};
213235

214236
const nextUrl = 'https://example.com/dashboard';
215-
base44.auth.redirectToLogin(nextUrl);
237+
clientWithoutAppBaseUrl.auth.redirectToLogin(nextUrl);
216238

217239
// Verify the redirect URL uses a relative path (no appBaseUrl prefix)
218240
expect(mockLocation.href).toBe(
@@ -316,33 +338,33 @@ describe('Auth Module', () => {
316338
global.window = {
317339
location: mockLocation
318340
};
319-
341+
320342
const redirectUrl = 'https://example.com/logout-success';
321343
base44.auth.logout(redirectUrl);
322-
323-
// Verify redirect
324-
expect(mockLocation.href).toBe(redirectUrl);
325-
344+
345+
// Verify redirect to server-side logout endpoint with from_url parameter
346+
const expectedUrl = `${appBaseUrl}/api/apps/auth/logout?from_url=${encodeURIComponent(redirectUrl)}`;
347+
expect(mockLocation.href).toBe(expectedUrl);
348+
326349
// Restore window
327350
global.window = originalWindow;
328351
});
329352

330-
test('should reload page when no redirect URL is provided', async () => {
331-
// Mock window object with reload function
332-
const mockReload = vi.fn();
353+
test('should redirect to logout endpoint when no redirect URL is provided', async () => {
354+
// Mock window object
355+
const mockLocation = { href: 'https://example.com/current-page' };
333356
const originalWindow = global.window;
334357
global.window = {
335-
location: {
336-
reload: mockReload
337-
}
358+
location: mockLocation
338359
};
339-
360+
340361
// Call logout without redirect URL
341362
base44.auth.logout();
342-
343-
// Verify page reload was called
344-
expect(mockReload).toHaveBeenCalledTimes(1);
345-
363+
364+
// Verify redirect to server-side logout endpoint with current page as from_url
365+
const expectedUrl = `${appBaseUrl}/api/apps/auth/logout?from_url=${encodeURIComponent('https://example.com/current-page')}`;
366+
expect(mockLocation.href).toBe(expectedUrl);
367+
346368
// Restore window
347369
global.window = originalWindow;
348370
});

tests/unit/client.test.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe('Client Creation', () => {
6464
serviceToken: 'service-token-123',
6565
requiresAuth: true,
6666
});
67-
67+
6868
expect(client).toBeDefined();
6969
expect(client.entities).toBeDefined();
7070
expect(client.integrations).toBeDefined();
@@ -78,6 +78,59 @@ describe('Client Creation', () => {
7878

7979
});
8080

81+
describe('appBaseUrl Normalization', () => {
82+
test('should use appBaseUrl when provided as a string', () => {
83+
const customAppBaseUrl = 'https://custom-app.example.com';
84+
const client = createClient({
85+
appId: 'test-app-id',
86+
appBaseUrl: customAppBaseUrl,
87+
});
88+
89+
// Mock window.location
90+
const originalWindow = global.window;
91+
const mockLocation = { href: '', origin: 'https://current-app.com' };
92+
global.window = {
93+
location: mockLocation
94+
};
95+
96+
const nextUrl = 'https://example.com/dashboard';
97+
client.auth.redirectToLogin(nextUrl);
98+
99+
// Verify the redirect URL uses the custom appBaseUrl
100+
expect(mockLocation.href).toBe(
101+
`${customAppBaseUrl}/login?from_url=${encodeURIComponent(nextUrl)}`
102+
);
103+
104+
// Restore window
105+
global.window = originalWindow;
106+
});
107+
108+
test('should normalize appBaseUrl to empty string when not provided', () => {
109+
const client = createClient({
110+
appId: 'test-app-id',
111+
// appBaseUrl not provided
112+
});
113+
114+
// Mock window.location
115+
const originalWindow = global.window;
116+
const mockLocation = { href: '', origin: 'https://current-app.com' };
117+
global.window = {
118+
location: mockLocation
119+
};
120+
121+
const nextUrl = 'https://example.com/dashboard';
122+
client.auth.redirectToLogin(nextUrl);
123+
124+
// Verify the redirect URL uses empty string (relative path)
125+
expect(mockLocation.href).toBe(
126+
`/login?from_url=${encodeURIComponent(nextUrl)}`
127+
);
128+
129+
// Restore window
130+
global.window = originalWindow;
131+
});
132+
});
133+
81134
describe('createClientFromRequest', () => {
82135
test('should create client from request with all headers', () => {
83136
const mockRequest = {

0 commit comments

Comments
 (0)