fix(web): surface SSO login request failures to the user (#43076)

Co-authored-by: SquabbyZ <43177939+SquabbyZ@users.noreply.github.com>
This commit is contained in:
Denis ErmakovandSquabbyZ authored and GitHub committed 2026-09-29 06:52:02 +00:00
1 parent 20d9f15933
commit bc0279572e
28 files changed
+146

No files matched your search

@@ -0,0 +1,100 @@
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import SSOAuth from '../sso-auth'
const mocks = vi.hoisted(() => ({
push: vi.fn(),
toastError: vi.fn(),
}))
vi.mock('@/next/navigation', () => ({
useRouter: () => ({ push: mocks.push }),
useSearchParams: () => new URLSearchParams(),
}))
vi.mock('@/app/notifications', () => ({
toast: { error: mocks.toastError },
}))
const jsonResponse = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
})
const clickSSOButton = async () => {
const user = userEvent.setup()
const button = screen.getByRole('button', { name: 'login.withSSO' })
await user.click(button)
return button
}
describe('SSOAuth', () => {
beforeEach(() => {
vi.restoreAllMocks()
vi.clearAllMocks()
})
it.each(['saml', 'oidc', 'oauth2'] as const)(
'should redirect to the %s provider url on success',
async (protocol) => {
const fetchSpy = vi
.spyOn(globalThis, 'fetch')
.mockResolvedValue(jsonResponse({ url: 'https://idp.example.com/login', state: 'state' }))
render(<SSOAuth protocol={protocol} />)
await clickSSOButton()
await waitFor(() => expect(mocks.push).toHaveBeenCalledWith('https://idp.example.com/login'))
expect(String((fetchSpy.mock.calls[0]![0] as Request).url)).toContain(
`/enterprise/sso/${protocol}/login`,
)
expect(mocks.toastError).not.toHaveBeenCalled()
},
)
it.each(['saml', 'oidc', 'oauth2'] as const)(
'should show an error and re-enable the button when the %s request fails',
async (protocol) => {
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new TypeError('Failed to fetch'))
render(<SSOAuth protocol={protocol} />)
const button = await clickSSOButton()
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('login.error.ssoFailed'))
expect(mocks.toastError).toHaveBeenCalledTimes(1)
expect(button).toBeEnabled()
expect(mocks.push).not.toHaveBeenCalled()
},
)
it('should show an error when the server responds without a JSON error message', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('<html>Bad Gateway</html>', {
status: 502,
headers: { 'Content-Type': 'text/html' },
}),
)
render(<SSOAuth protocol="saml" />)
const button = await clickSSOButton()
await waitFor(() => expect(button).toBeEnabled())
expect(mocks.toastError).toHaveBeenCalledTimes(1)
expect(mocks.toastError).toHaveBeenCalledWith('login.error.ssoFailed')
})
it('should only show the server message when the error response carries one', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
jsonResponse({ message: 'SSO provider unavailable' }, 500),
)
render(<SSOAuth protocol="saml" />)
const button = await clickSSOButton()
await waitFor(() => expect(button).toBeEnabled())
expect(mocks.toastError).toHaveBeenCalledTimes(1)
expect(mocks.toastError).toHaveBeenCalledWith('SSO provider unavailable')
})
})
+20
View File
@@ -13,6 +13,18 @@ type SSOAuthProps = {
protocol: SsoProtocol
}
// The request layer already toasts HTTP errors whose JSON body carries a message.
const hasNotifiedRequestError = async (error: unknown) => {
if (!(error instanceof Response) || error.status === 401) return false
try {
const body: unknown = await error.clone().json()
if (!body || typeof body !== 'object') return false
return ('message' in body && !!body.message) || ('error' in body && !!body.error)
} catch {
return false
}
}
const SSOAuth: FC<SSOAuthProps> = ({ protocol }) => {
const router = useRouter()
const { t } = useTranslation(['login'])
@@ -21,6 +33,11 @@ const SSOAuth: FC<SSOAuthProps> = ({ protocol }) => {
const [isLoading, setIsLoading] = useState(false)
const handleSSOError = async (error: unknown) => {
if (await hasNotifiedRequestError(error)) return
toast.error(t(($) => $['error.ssoFailed'], { ns: 'login' }))
}
const handleSSOLogin = () => {
setIsLoading(true)
if (protocol === zSsoProtocol.enum.saml) {
@@ -28,6 +45,7 @@ const SSOAuth: FC<SSOAuthProps> = ({ protocol }) => {
.then((res) => {
router.push(res.url)
})
.catch(handleSSOError)
.finally(() => {
setIsLoading(false)
})
@@ -37,6 +55,7 @@ const SSOAuth: FC<SSOAuthProps> = ({ protocol }) => {
document.cookie = `user-oidc-state=${res.state};Path=/`
router.push(res.url)
})
.catch(handleSSOError)
.finally(() => {
setIsLoading(false)
})
@@ -46,6 +65,7 @@ const SSOAuth: FC<SSOAuthProps> = ({ protocol }) => {
document.cookie = `user-oauth2-state=${res.state};Path=/`
router.push(res.url)
})
.catch(handleSSOError)
.finally(() => {
setIsLoading(false)
})
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "كلمة المرور مطلوبة",
"error.passwordInvalid": "يجب أن تحتوي كلمة المرور على أحرف وأرقام، ويجب أن يكون الطول أكبر من 8",
"error.registrationNotAllowed": "الحساب غير موجود. يرجى الاتصال بمسؤول النظام للتسجيل.",
"error.ssoFailed": "فشل تسجيل الدخول عبر SSO. يرجى المحاولة مرة أخرى أو الاتصال بالمسؤول.",
"explore": "استكشاف Dify",
"forget": "نسيت كلمة المرور؟",
"forgotPassword": "نسيت كلمة المرور؟",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Parol tələb olunur",
"error.passwordInvalid": "Parolda hərflər və rəqəmlər olmalı, uzunluğu isə 8 simvoldan çox olmalıdır",
"error.registrationNotAllowed": "Hesab tapılmadı. Qeydiyyatdan keçmək üçün sistem inzibatçısı ilə əlaqə saxlayın.",
"error.ssoFailed": "SSO ilə giriş uğursuz oldu. Yenidən cəhd edin və ya administratorla əlaqə saxlayın.",
"explore": "Dify ilə tanış ol",
"forget": "Parolunuzu unutmusunuz?",
"forgotPassword": "Parolunuzu unutmusunuz?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Passwort wird benötigt",
"error.passwordInvalid": "Das Passwort muss Buchstaben und Zahlen enthalten und länger als 8 Zeichen sein",
"error.registrationNotAllowed": "Konto nicht gefunden. Bitte wenden Sie sich an den Systemadministrator, um sich zu registrieren.",
"error.ssoFailed": "SSO-Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut oder wenden Sie sich an Ihren Administrator.",
"explore": "Dify erkunden",
"forget": "Passwort vergessen?",
"forgotPassword": "Passwort vergessen?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Password is required",
"error.passwordInvalid": "Password must contain letters and numbers, and the length must be greater than 8",
"error.registrationNotAllowed": "Account not found. Please contact the system admin to register.",
"error.ssoFailed": "SSO login failed. Please try again or contact your administrator.",
"explore": "Explore Dify",
"forget": "Forgot your password?",
"forgotPassword": "Forgot your password?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Se requiere una contraseña",
"error.passwordInvalid": "La contraseña debe contener letras y números, y tener una longitud mayor a 8",
"error.registrationNotAllowed": "Cuenta no encontrada. Póngase en contacto con el administrador del sistema para registrarse.",
"error.ssoFailed": "Error al iniciar sesión con SSO. Inténtelo de nuevo o póngase en contacto con su administrador.",
"explore": "Explorar Dify",
"forget": "¿Olvidaste tu contraseña?",
"forgotPassword": "¿Olvidaste tu contraseña?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "رمز عبور لازم است",
"error.passwordInvalid": "رمز عبور باید شامل حروف و اعداد باشد و طول آن بیشتر از ۸ کاراکتر باشد",
"error.registrationNotAllowed": "حساب پیدا نشد. لطفا برای ثبت نام با مدیر سیستم تماس حاصل فرمایید.",
"error.ssoFailed": "ورود با SSO ناموفق بود. لطفاً دوباره تلاش کنید یا با مدیر سیستم تماس بگیرید.",
"explore": "کاوش Dify",
"forget": "رمز عبور خود را فراموش کرده‌اید؟",
"forgotPassword": "رمز عبور خود را فراموش کرده‌اید؟",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Un mot de passe est requis",
"error.passwordInvalid": "Le mot de passe doit contenir des lettres et des chiffres, et la longueur doit être supérieure à 8.",
"error.registrationNotAllowed": "Compte introuvable. Veuillez contacter l’administrateur système pour vous inscrire.",
"error.ssoFailed": "Échec de la connexion SSO. Veuillez réessayer ou contacter votre administrateur.",
"explore": "Explorez Dify",
"forget": "Mot de passe oublié ?",
"forgotPassword": "Mot de passe oublié?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "पासवर्ड आवश्यक है",
"error.passwordInvalid": "पासवर्ड में अक्षर और अंक होने चाहिए, और लंबाई 8 से अधिक होनी चाहिए",
"error.registrationNotAllowed": "खाता नहीं मिला. रजिस्टर करने के लिए कृपया सिस्टम एडमिन से संपर्क करें।",
"error.ssoFailed": "SSO लॉगिन विफल रहा। कृपया पुनः प्रयास करें या अपने व्यवस्थापक से संपर्क करें।",
"explore": "Dify का अन्वेषण करें",
"forget": "क्या आप पासवर्ड भूल गए?",
"forgotPassword": "क्या आपने अपना पासवर्ड भूल गए हैं?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Kata sandi diperlukan",
"error.passwordInvalid": "Kata sandi harus berisi huruf dan angka, dan panjangnya harus lebih besar dari 8",
"error.registrationNotAllowed": "Akun tidak ditemukan. Silakan hubungi admin sistem untuk mendaftar.",
"error.ssoFailed": "Login SSO gagal. Silakan coba lagi atau hubungi administrator Anda.",
"explore": "Jelajahi Dify",
"forget": "Lupa Kata Sandi Anda?",
"forgotPassword": "Lupa Kata Sandi Anda?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "La password è obbligatoria",
"error.passwordInvalid": "La password deve contenere lettere e numeri, e la lunghezza deve essere maggiore di 8",
"error.registrationNotAllowed": "Account non trovato. Si prega di contattare l'amministratore di sistema per registrarsi.",
"error.ssoFailed": "Accesso SSO non riuscito. Riprova o contatta l'amministratore.",
"explore": "Esplora Dify",
"forget": "Hai dimenticato la password?",
"forgotPassword": "Hai dimenticato la password?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "パスワードは必須です",
"error.passwordInvalid": "パスワードは文字と数字を含み、長さは 8 以上である必要があります",
"error.registrationNotAllowed": "アカウントが見つかりません。登録するためにシステム管理者に連絡してください。",
"error.ssoFailed": "SSO ログインに失敗しました。もう一度お試しいただくか、管理者にお問い合わせください。",
"explore": "Dify を探索する",
"forget": "パスワードをお忘れですか?",
"forgotPassword": "パスワードを忘れましたか?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "비밀번호를 입력하세요.",
"error.passwordInvalid": "비밀번호는 문자와 숫자를 포함하고 8 자 이상이어야 합니다.",
"error.registrationNotAllowed": "계정을 찾을 수 없습니다. 등록하려면 시스템 관리자에게 문의하십시오.",
"error.ssoFailed": "SSO 로그인에 실패했습니다. 다시 시도하거나 관리자에게 문의하세요.",
"explore": "Dify 를 탐색하세요",
"forget": "비밀번호를 잊으셨나요?",
"forgotPassword": "비밀번호를 잊으셨나요?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "ກະລຸນາປ້ອນລະຫັດຜ່ານ",
"error.passwordInvalid": "ລະຫັດຜ່ານຕ້ອງປະກອບມີຕົວອັກສອນ ແລະ ຕົວເລກ, ແລະ ຕ້ອງມີຄວາມຍາວຫຼາຍກວ່າ 8 ຕົວອັກສອນ",
"error.registrationNotAllowed": "ບໍ່ພົບບັນຊີ. ກະລຸນາຕິດຕໍ່ຜູ້ດູແລລະບົບເພື່ອລົງທະບຽນ.",
"error.ssoFailed": "ການເຂົ້າສູ່ລະບົບ SSO ລົ້ມເຫຼວ. ກະລຸນາລອງໃໝ່ ຫຼື ຕິດຕໍ່ຜູ້ດູແລລະບົບຂອງທ່ານ.",
"explore": "ສຳຫຼວດ Dify",
"forget": "ລືມລະຫັດຜ່ານ?",
"forgotPassword": "ລືມລະຫັດຜ່ານ?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Password is required",
"error.passwordInvalid": "Password must contain letters and numbers, and the length must be greater than 8",
"error.registrationNotAllowed": "Account not found. Please contact the system admin to register.",
"error.ssoFailed": "SSO-aanmelding mislukt. Probeer het opnieuw of neem contact op met je beheerder.",
"explore": "Explore Dify",
"forget": "Forgot your password?",
"forgotPassword": "Forgot your password?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Hasło jest wymagane",
"error.passwordInvalid": "Hasło musi zawierać litery i cyfry, a jego długość musi być większa niż 8",
"error.registrationNotAllowed": "Nie znaleziono konta. Skontaktuj się z administratorem systemu, aby się zarejestrować.",
"error.ssoFailed": "Logowanie SSO nie powiodło się. Spróbuj ponownie lub skontaktuj się z administratorem.",
"explore": "Odkryj Dify",
"forget": "Zapomniałeś hasła?",
"forgotPassword": "Zapomniałeś hasła?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "A senha é obrigatória",
"error.passwordInvalid": "A senha deve conter letras e números e ter um comprimento maior que 8",
"error.registrationNotAllowed": "Conta não encontrada. Entre em contato com o administrador do sistema para se registrar.",
"error.ssoFailed": "Falha no login via SSO. Tente novamente ou entre em contato com o administrador.",
"explore": "Explorar o Dify",
"forget": "Esqueceu sua senha?",
"forgotPassword": "Esqueceu sua senha?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Parola este obligatorie",
"error.passwordInvalid": "Parola trebuie să conțină litere și cifre, iar lungimea trebuie să fie mai mare de 8 caractere",
"error.registrationNotAllowed": "Contul nu a fost găsit. Vă rugăm să contactați administratorul de sistem pentru a vă înregistra.",
"error.ssoFailed": "Autentificarea SSO a eșuat. Încercați din nou sau contactați administratorul.",
"explore": "Explorează Dify",
"forget": "Ai uitat parola?",
"forgotPassword": "Ați uitat parola?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Пароль обязателен",
"error.passwordInvalid": "Пароль должен содержать буквы и цифры, а длина должна быть больше 8",
"error.registrationNotAllowed": "Аккаунт не найден. Пожалуйста, свяжитесь с системным администратором для регистрации.",
"error.ssoFailed": "Не удалось войти через SSO. Попробуйте ещё раз или обратитесь к администратору.",
"explore": "Изучить Dify",
"forget": "Забыли пароль?",
"forgotPassword": "Забыли пароль?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Geslo je obvezno",
"error.passwordInvalid": "Geslo mora vsebovati črke in številke, dolžina pa mora biti več kot 8 znakov",
"error.registrationNotAllowed": "Računa ni mogoče najti. Za registracijo se obrnite na skrbnika sistema.",
"error.ssoFailed": "Prijava SSO ni uspela. Poskusite znova ali se obrnite na skrbnika.",
"explore": "Razišči Dify",
"forget": "Ste pozabili geslo?",
"forgotPassword": "Ste pozabili geslo?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "ต้องใช้รหัสผ่าน",
"error.passwordInvalid": "รหัสผ่านต้องมีตัวอักษรและตัวเลข และความยาวต้องมากกว่า 8",
"error.registrationNotAllowed": "ไม่พบบัญชี โปรดติดต่อผู้ดูแลระบบเพื่อลงทะเบียน",
"error.ssoFailed": "เข้าสู่ระบบด้วย SSO ไม่สำเร็จ โปรดลองอีกครั้งหรือติดต่อผู้ดูแลระบบของคุณ",
"explore": "สํารวจ Dify",
"forget": "ลืมรหัสผ่านใช่ไหม",
"forgotPassword": "ลืมรหัสผ่านใช่ไหม",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Şifre gereklidir",
"error.passwordInvalid": "Şifre harf ve rakamlardan oluşmalı ve uzunluğu 8 karakterden fazla olmalıdır",
"error.registrationNotAllowed": "Hesap bulunamadı. Kayıt olmak için lütfen sistem yöneticisi ile iletişime geçin.",
"error.ssoFailed": "SSO ile giriş başarısız oldu. Lütfen tekrar deneyin veya yöneticinize başvurun.",
"explore": "Dify'yi Keşfet",
"forget": "Şifrenizi mi unuttunuz?",
"forgotPassword": "Şifrenizi mi unuttunuz?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Пароль є обов’язковим",
"error.passwordInvalid": "Пароль повинен містити літери та цифри, а довжина повинна бути більшою за 8",
"error.registrationNotAllowed": "Аккаунт не знайдено. Будь ласка, зверніться до адміністратора системи для реєстрації.",
"error.ssoFailed": "Не вдалося увійти через SSO. Спробуйте ще раз або зверніться до адміністратора.",
"explore": "Досліджуйте Dify",
"forget": "Забули пароль?",
"forgotPassword": "Забули пароль?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "Vui lòng nhập mật khẩu",
"error.passwordInvalid": "Mật khẩu phải chứa cả chữ và số, và có độ dài ít nhất 8 ký tự",
"error.registrationNotAllowed": "Không tìm thấy tài khoản. Vui lòng liên hệ với quản trị viên hệ thống để đăng ký.",
"error.ssoFailed": "Đăng nhập SSO không thành công. Vui lòng thử lại hoặc liên hệ với quản trị viên.",
"explore": "Khám phá Dify",
"forget": "Quên mật khẩu?",
"forgotPassword": "Quên mật khẩu?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "密码不能为空",
"error.passwordInvalid": "密码必须包含字母和数字,且长度不小于 8 位",
"error.registrationNotAllowed": "账户不存在,请联系系统管理员注册账户",
"error.ssoFailed": "SSO 登录失败。请重试或联系管理员。",
"explore": "探索 Dify",
"forget": "忘记密码?",
"forgotPassword": "忘记密码?",
+1
View File
@@ -31,6 +31,7 @@
"error.passwordEmpty": "密碼不能為空",
"error.passwordInvalid": "密碼必須包含字母和數字,且長度不小於 8 位",
"error.registrationNotAllowed": "找不到帳戶。請聯繫系統管理員進行註冊。",
"error.ssoFailed": "SSO 登入失敗。請重試或聯絡管理員。",
"explore": "探索 Dify",
"forget": "忘記密碼?",
"forgotPassword": "忘記密碼?",
+1
View File
@@ -3217,6 +3217,7 @@ export default interface Resources {
'error.passwordEmpty': 'Password is required'
'error.passwordInvalid': 'Password must contain letters and numbers, and the length must be greater than 8'
'error.registrationNotAllowed': 'Account not found. Please contact the system admin to register.'
'error.ssoFailed': 'SSO login failed. Please try again or contact your administrator.'
explore: 'Explore Dify'
forget: 'Forgot your password?'
forgotPassword: 'Forgot your password?'