mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 00:00:33 +00:00
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:
@@ -1 +1 @@
|
||||
f6bc5647cc41ce348111f781b4a0db2153a28f8a
|
||||
b7157d55fb9f8d8f7aeb7b1fb69bc935af895a2f
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user