diff --git a/app/proprietary/src/main/resources/static/index.html b/app/proprietary/src/main/resources/static/index.html
new file mode 100644
index 0000000000..0dc414a279
--- /dev/null
+++ b/app/proprietary/src/main/resources/static/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+ Stirling PDF - Authentication
+
+
+
+ Completing authentication...
+
+
+
diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java
index a7f8042f6f..3f0214573f 100644
--- a/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java
+++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java
@@ -51,7 +51,8 @@ class UserLicenseSettingsServiceTest {
when(applicationProperties.getPremium()).thenReturn(premium);
when(applicationProperties.getAutomaticallyGenerated()).thenReturn(automaticallyGenerated);
- when(automaticallyGenerated.getIsNewServer()).thenReturn(false); // Default: not a new server
+ when(automaticallyGenerated.getIsNewServer())
+ .thenReturn(false); // Default: not a new server
when(settingsRepository.findSettings()).thenReturn(Optional.of(mockSettings));
when(userService.getTotalUsersCount()).thenReturn(80L);
when(settingsRepository.save(any(UserLicenseSettings.class)))
diff --git a/frontend/src-tauri/capabilities/main.json b/frontend/src-tauri/capabilities/main.json
new file mode 100644
index 0000000000..b1ac19e44d
--- /dev/null
+++ b/frontend/src-tauri/capabilities/main.json
@@ -0,0 +1,19 @@
+{
+ "identifier": "main",
+ "description": "Main window capability with remote access for self-hosted SSO.",
+ "windows": ["main"],
+ "permissions": [
+ "core:default",
+ "http:default",
+ "http:allow-fetch",
+ "http:allow-fetch-send",
+ "http:allow-fetch-read-body",
+ "opener:default",
+ "opener:allow-open-url",
+ "core:webview:allow-internal-toggle-devtools",
+ "core:webview:allow-create-webview-window"
+ ],
+ "remote": {
+ "urls": ["http://localhost:8080/*"]
+ }
+}
diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json
index b10fec0e54..3f8bc1a7c3 100644
--- a/frontend/src-tauri/tauri.conf.json
+++ b/frontend/src-tauri/tauri.conf.json
@@ -18,7 +18,10 @@
"resizable": true,
"fullscreen": false
}
- ]
+ ],
+ "security": {
+ "capabilities": ["main"]
+ }
},
"bundle": {
"active": true,
diff --git a/frontend/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx b/frontend/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx
index 70a44d9090..e1d3f75b35 100644
--- a/frontend/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx
+++ b/frontend/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Text } from '@mantine/core';
import LoginHeader from '@app/routes/login/LoginHeader';
@@ -51,10 +51,54 @@ export const SelfHostedLoginScreen: React.FC = ({
setValidationError(errorMessage);
};
- const waitForSsoCompletion = (popup: Window): Promise => {
+ const pollForSession = (timeoutMs = 60000): Promise => {
+ return new Promise((resolve, reject) => {
+ const start = Date.now();
+ const interval = window.setInterval(async () => {
+ if (Date.now() - start > timeoutMs) {
+ window.clearInterval(interval);
+ reject(new Error('Timed out waiting for authentication'));
+ return;
+ }
+
+ try {
+ const hasToken = localStorage.getItem('stirling_jwt');
+ if (hasToken) {
+ window.clearInterval(interval);
+ resolve();
+ return;
+ }
+
+ const isAuthed = await authService.isAuthenticated();
+ if (isAuthed) {
+ window.clearInterval(interval);
+ resolve();
+ }
+ } catch (e) {
+ // Ignore transient errors and keep polling
+ }
+ }, 1000);
+ });
+ };
+
+ const waitForSsoCompletion = (popup: Window, expectedOrigin?: string): Promise => {
+ // Accept messages from the backend origin (e.g., http://localhost:8080) since the wizard runs under the Tauri origin.
+ const allowedOrigin = (() => {
+ try {
+ return expectedOrigin ? new URL(expectedOrigin).origin : null;
+ } catch (_) {
+ return null;
+ }
+ })();
+
return new Promise((resolve, reject) => {
const messageHandler = (event: MessageEvent) => {
- if (event.origin !== window.location.origin || typeof event.data !== 'object' || event.data === null) {
+ if (typeof event.data !== 'object' || event.data === null) {
+ return;
+ }
+
+ // If we can compute the backend origin, ensure the message comes from there.
+ if (allowedOrigin && event.origin !== allowedOrigin) {
return;
}
@@ -95,30 +139,9 @@ export const SelfHostedLoginScreen: React.FC = ({
// Mark SSO flow so the callback can short-circuit verification
localStorage.setItem('desktop_sso_in_progress', JSON.stringify({ mode: 'selfhosted' }));
- const popup = window.open(provider.url, '_blank', 'width=480,height=720');
- if (!popup) {
- localStorage.removeItem('desktop_sso_in_progress');
- handleOAuthError(t('setup.login.error.oauthFailed', 'OAuth login failed. Please try again.'));
- return;
- }
-
- try {
- const token = await waitForSsoCompletion(popup);
- localStorage.removeItem('desktop_sso_in_progress');
-
- await authService.applyExternalToken(token);
- await onOAuthSuccess({ username: provider.label || provider.id });
- } catch (err) {
- localStorage.removeItem('desktop_sso_in_progress');
- const message = err instanceof Error ? err.message : t('setup.login.error.oauthFailed', 'OAuth login failed. Please try again.');
- handleOAuthError(message);
- } finally {
- try {
- popup.close();
- } catch (_) {
- // ignore
- }
- }
+ console.debug('[Desktop SSO] Launching provider (same-window)', provider);
+ // Navigate the current webview to the provider URL; the callback page will store the token and reload the app.
+ window.location.assign(provider.url);
};
const displayError = error || validationError;