feat: add auto-login SSO provider instance setting (#8929)

* [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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
Ruben Fiszel
2026-04-23 21:20:01 -07:00
committed by GitHub
parent 161ec8d722
commit 4cf53a44bb
7 changed files with 78 additions and 28 deletions
+1 -1
View File
@@ -1 +1 @@
f6bc5647cc41ce348111f781b4a0db2153a28f8a
b7157d55fb9f8d8f7aeb7b1fb69bc935af895a2f
+3
View File
@@ -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
+2 -1
View File
@@ -92,11 +92,12 @@ pub struct TokenResponse {
struct Logins {
oauth: Vec<String>,
saml: Option<String>,
auto_login: Option<String>,
}
#[cfg(not(feature = "private"))]
async fn list_logins() -> error::JsonResult<Logins> {
// 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)]
@@ -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";
+61 -25
View File
@@ -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)
})
</script>
<div class="bg-surface px-4 py-8 border sm:rounded-lg sm:px-10">
<div class="grid {logins && logins.length > 2 ? 'grid-cols-2' : ''} gap-4">
{#if autoRedirecting}
<p class="text-sm text-center text-secondary py-4">Signing you in…</p>
{/if}
<div
class="grid {logins && logins.length > 2 ? 'grid-cols-2' : ''} gap-4 {autoRedirecting
? 'hidden'
: ''}"
>
{#if !logins}
{#each Array(4) as _}
<Skeleton layout={[0.5, [2.375]]} />
@@ -355,29 +410,10 @@
{/each}
{/if}
{#if saml}
<Button
variant="default"
btnClasses="mt-2 w-full"
on:click={() => {
if (saml) {
if (rd) {
try {
localStorage.setItem('rd', rd)
} catch (e) {
console.error('Could not persist redirection to local storage', e)
}
}
window.location.href = saml
} else {
sendUserToast('No SAML login available', true)
}
}}
>
SSO
</Button>
<Button variant="default" btnClasses="mt-2 w-full" on:click={redirectSaml}>SSO</Button>
{/if}
</div>
{#if !disablePasswordLogin && (saml || (logins && logins.length > 0))}
{#if !autoRedirecting && !disablePasswordLogin && (saml || (logins && logins.length > 0))}
<div class={classNames('center-center', logins && logins.length > 0 ? 'mt-6' : '')}>
<Button
size="xs"
@@ -391,7 +427,7 @@
</div>
{/if}
{#if showPassword && !disablePasswordLogin}
{#if !autoRedirecting && showPassword && !disablePasswordLogin}
<div>
{#if firstTime}
<p class="text-xs text-center w-full pb-4 text-secondary">
@@ -388,6 +388,15 @@ export const settings: Record<string, Setting[]> = {
key: 'disable_password_login',
fieldType: 'boolean',
storage: 'setting'
},
{
label: 'Auto-login SSO provider',
description:
'If set, the login page redirects automatically to this provider. Use the OAuth provider key (e.g. "okta", "google") or "saml". The provider must be configured; otherwise the setting is ignored. Visit /user/login?no_sso=1 to bypass the redirect and fall back to the normal login form.',
key: 'auto_login_provider',
fieldType: 'text',
placeholder: 'okta',
storage: 'setting'
}
],
'DB Health': [],
@@ -152,6 +152,6 @@
<div class="flex justify-end">
<DarkModeToggle forcedDarkMode={false} />
</div>
<Login {firstTime} {rd} {error} {password} {email} />
<Login {firstTime} {rd} {error} {password} {email} autoRedirect={false} />
</div>
</div>