From 4cf53a44bb10b65dcdb45bac97186a10cdbb48d6 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 23 Apr 2026 21:20:01 -0700 Subject: [PATCH] feat: add auto-login SSO provider instance setting (#8929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [ee] feat: add auto-login SSO provider instance setting Adds an instance-level `auto_login_provider` setting that, when set to an OAuth provider key (e.g. "okta") or "saml", causes the login page to auto-redirect users to the configured SSO flow on mount. Useful for orgs with a single SSO where the provider button grid adds a pointless extra click. - Backend: new global setting constant, read from DB in list_logins handler and returned as the `auto_login` field in the response - Frontend: Login.svelte auto-redirects in loadLogins() when the configured provider is actually present in the response - Escape hatch: `?no_sso=1` skips the auto-redirect and shows the normal login form (admin fallback when SSO is broken) - No redirect loop: if the `error` prop is set (SSO callback failed), the redirect is skipped - Admin UI: new text field under Auth/OAuth/SAML in instance settings Co-Authored-By: Claude Opus 4.7 (1M context) * fix: skip auto-redirect on /user/login page Auto-redirect should only fire on embeds where the user did not explicitly navigate to a login screen (public app popup, approval pages). Visiting /user/login is an explicit sign-in action — often by an admin who needs password fallback — so we must never hijack it. Gate the logic on a new `autoRedirect` prop (default true). The main login page passes `autoRedirect={false}`. Co-Authored-By: Claude Opus 4.7 (1M context) * chore: update ee-repo-ref to b7157d55fb9f8d8f7aeb7b1fb69bc935af895a2f This commit updates the EE repository reference after PR #547 was merged in windmill-ee-private. Previous ee-repo-ref: e32a48499d206a24e0c12817b465775321b0ee41 New ee-repo-ref: b7157d55fb9f8d8f7aeb7b1fb69bc935af895a2f Automated by sync-ee-ref workflow. * fix: handle popup-blocked auto-redirect in popup mode When Login is embedded with popup=true (public app), auto-redirect funnels through window.open() without a user gesture — browsers block it by default, leaving the user stuck on "Signing you in…". Detect window.open returning null, clean up listeners, reset autoRedirecting so the provider button grid re-renders, and surface a toast pointing users at the manual button. The grid click retains its user gesture and passes the popup blocker. Also extracts a redirectSaml() helper so the SAML auto-redirect path and the SSO button click share the same logic. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: windmill-internal-app[bot] --- backend/ee-repo-ref.txt | 2 +- backend/windmill-api/openapi.yaml | 3 + backend/windmill-api/src/oauth2_oss.rs | 3 +- .../windmill-common/src/global_settings.rs | 1 + frontend/src/lib/components/Login.svelte | 86 +++++++++++++------ .../src/lib/components/instanceSettings.ts | 9 ++ .../(logged)/user/(user)/login/+page.svelte | 2 +- 7 files changed, 78 insertions(+), 28 deletions(-) diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 1bc481aad7..34fceaa918 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -f6bc5647cc41ce348111f781b4a0db2153a28f8a +b7157d55fb9f8d8f7aeb7b1fb69bc935af895a2f diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 5212392dd4..f1318e8569 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -5966,6 +5966,9 @@ paths: - type saml: type: string + auto_login: + type: string + description: provider type to auto-redirect to on login (oauth key or "saml") required: - oauth diff --git a/backend/windmill-api/src/oauth2_oss.rs b/backend/windmill-api/src/oauth2_oss.rs index 3eaf179634..abdcc42548 100644 --- a/backend/windmill-api/src/oauth2_oss.rs +++ b/backend/windmill-api/src/oauth2_oss.rs @@ -92,11 +92,12 @@ pub struct TokenResponse { struct Logins { oauth: Vec, saml: Option, + auto_login: Option, } #[cfg(not(feature = "private"))] async fn list_logins() -> error::JsonResult { // Implementation is not open source - return Ok(Json(Logins { oauth: vec![], saml: None })); + return Ok(Json(Logins { oauth: vec![], saml: None, auto_login: None })); } #[allow(unused)] diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 836d37971e..6e6486666c 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -50,6 +50,7 @@ pub const HUB_API_SECRET_SETTING: &str = "hub_api_secret"; pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation"; pub const DISABLE_PASSWORD_LOGIN_SETTING: &str = "disable_password_login"; +pub const AUTO_LOGIN_PROVIDER_SETTING: &str = "auto_login_provider"; pub const HUB_BASE_URL_SETTING: &str = "hub_base_url"; pub const HUB_ACCESSIBLE_URL_SETTING: &str = "hub_accessible_url"; pub const DISABLE_HUB_SETTING: &str = "disable_hub"; diff --git a/frontend/src/lib/components/Login.svelte b/frontend/src/lib/components/Login.svelte index 091676bde7..b737ef8513 100644 --- a/frontend/src/lib/components/Login.svelte +++ b/frontend/src/lib/components/Login.svelte @@ -30,6 +30,7 @@ error?: string | undefined popup?: boolean firstTime?: boolean + autoRedirect?: boolean onLoginSuccess?: () => void } @@ -40,6 +41,7 @@ error = undefined, popup = false, firstTime = false, + autoRedirect = true, onLoginSuccess = undefined }: Props = $props() @@ -93,6 +95,7 @@ let saml: string | undefined = $state(undefined) let smtpConfigured: boolean | undefined = $state(undefined) let disablePasswordLogin = $state(false) + let autoRedirecting = $state(false) type OAuthLogin = { type: string @@ -201,12 +204,14 @@ console.error('Could not load password login setting', disabledResult.reason) } + let autoLogin: string | undefined = undefined if (loginsResult.status === 'fulfilled') { logins = loginsResult.value.oauth.map((login) => ({ type: login.type, displayName: login.display_name || login.type })) saml = loginsResult.value.saml + autoLogin = loginsResult.value.auto_login } else { logins = [] saml = undefined @@ -216,6 +221,28 @@ showPassword = !disablePasswordLogin && ((logins?.length === 0 && !saml) || (email != undefined && email.length > 0)) + + if (autoRedirect && autoLogin && !error && !shouldSkipAutoRedirect()) { + if (autoLogin === 'saml' && saml) { + autoRedirecting = true + if (!redirectSaml()) autoRedirecting = false + } else if (logins?.some((l) => l.type === autoLogin)) { + autoRedirecting = true + if (!storeRedirect(autoLogin)) { + autoRedirecting = false + sendUserToast('Popup blocked — please click the sign-in button to continue.', true) + } + } + } + } + + function shouldSkipAutoRedirect(): boolean { + try { + const params = new URLSearchParams(window.location.search) + return params.get('no_sso') === '1' + } catch { + return false + } } loadLogins() @@ -299,7 +326,7 @@ window.removeEventListener('storage', handleStorageEvent) }) - function storeRedirect(provider: string) { + function persistRd() { if (rd) { try { localStorage.setItem('rd', rd) @@ -307,6 +334,10 @@ console.error('Could not persist redirection to local storage', e) } } + } + + function storeRedirect(provider: string): boolean { + persistRd() let url = base + '/api/oauth/login/' + provider + (popup ? '?close=true' : '') console.log('storeRedirect', popup, url) @@ -314,20 +345,44 @@ localStorage.setItem('closeUponLogin', 'true') window.addEventListener('message', popupListener) window.addEventListener('storage', handleStorageEvent) - window.open(url, '_blank', 'popup') + const win = window.open(url, '_blank', 'popup') + if (!win) { + window.removeEventListener('message', popupListener) + window.removeEventListener('storage', handleStorageEvent) + return false + } + return true } else { localStorage.setItem('closeUponLogin', 'false') window.location.href = url + return true } } + function redirectSaml(): boolean { + if (!saml) { + sendUserToast('No SAML login available', true) + return false + } + persistRd() + window.location.href = saml + return true + } + $effect(() => { error && sendUserToast(escapeHtml(error), true) })
-
+ {#if autoRedirecting} +

Signing you in…

+ {/if} +
{#if !logins} {#each Array(4) as _} @@ -355,29 +410,10 @@ {/each} {/if} {#if saml} - + {/if}
- {#if !disablePasswordLogin && (saml || (logins && logins.length > 0))} + {#if !autoRedirecting && !disablePasswordLogin && (saml || (logins && logins.length > 0))}
0 ? 'mt-6' : '')}>