diff --git a/apps/sim/ee/sso/components/sso-form.test.tsx b/apps/sim/ee/sso/components/sso-form.test.tsx
index 7a6f2dc0af7..88a010706d6 100644
--- a/apps/sim/ee/sso/components/sso-form.test.tsx
+++ b/apps/sim/ee/sso/components/sso-form.test.tsx
@@ -1,11 +1,13 @@
/**
* @vitest-environment jsdom
*/
-import type { ReactNode } from 'react'
+import { act, type ButtonHTMLAttributes, type InputHTMLAttributes, type ReactNode } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
import { renderToString } from 'react-dom/server'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-const { mockUseSearchParams } = vi.hoisted(() => ({
+const { mockSsoSignIn, mockUseSearchParams } = vi.hoisted(() => ({
+ mockSsoSignIn: vi.fn(),
mockUseSearchParams: vi.fn(),
}))
@@ -21,19 +23,35 @@ vi.mock('next/link', () => ({
}))
vi.mock('@sim/emcn', () => ({
- Button: ({ children }: { children?: ReactNode }) => ,
- Input: () => ,
+ Button: ({ children, ...props }: ButtonHTMLAttributes) => (
+
+ ),
+ Input: (props: InputHTMLAttributes) => ,
Label: ({ children }: { children?: ReactNode }) => {children},
cn: (...values: unknown[]) => values.filter(Boolean).join(' '),
}))
vi.mock('@/lib/auth/auth-client', () => ({
- client: { signIn: { sso: vi.fn() } },
+ client: { signIn: { sso: mockSsoSignIn } },
}))
vi.mock('@/app/(auth)/components', () => ({
- AuthSubmitButton: ({ children }: { children?: ReactNode }) => (
-
+ AuthSubmitButton: ({
+ children,
+ disabled = false,
+ loading = false,
+ loadingLabel,
+ }: {
+ children?: ReactNode
+ disabled?: boolean
+ loading?: boolean
+ loadingLabel: string
+ }) => (
+
),
}))
@@ -49,6 +67,22 @@ function renderFirstFrame(search: string, registrationDisabled = false): string
return renderToString()
}
+let container: HTMLDivElement
+let root: Root
+
+function renderInteractive(search = '') {
+ mockUseSearchParams.mockReturnValue(new URLSearchParams(search))
+ act(() => root.render())
+}
+
+async function submitForm() {
+ const form = container.querySelector('form')
+ expect(form).not.toBeNull()
+ await act(async () => {
+ form?.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
+ })
+}
+
/**
* `renderToString` produces the markup of the first frame with no effects run,
* which is exactly the window in which a callback URL seeded from an effect is
@@ -99,3 +133,55 @@ describe('SSOForm signup cross-link', () => {
expect(html).not.toContain('/signup')
})
})
+
+describe('SSOForm sign-in errors', () => {
+ beforeEach(() => {
+ mockSsoSignIn.mockReset()
+ mockUseSearchParams.mockReset()
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+ })
+
+ afterEach(() => {
+ act(() => root.unmount())
+ container.remove()
+ })
+
+ it('shows a generic retryable error when Better Auth resolves with a 404', async () => {
+ mockSsoSignIn.mockResolvedValue({
+ data: null,
+ error: {
+ message: 'No provider found for the issuer',
+ status: 404,
+ statusText: 'Not Found',
+ },
+ })
+ renderInteractive('email=user%40example.com')
+
+ await submitForm()
+
+ expect(container).toHaveTextContent('Unable to start SSO. Check your email and try again.')
+ expect(container).not.toHaveTextContent('No provider found for the issuer')
+ const submitButton = container.querySelector('button[type="submit"]')
+ expect(submitButton?.disabled).toBe(false)
+ expect(submitButton).toHaveTextContent('Continue with SSO')
+
+ await submitForm()
+ expect(mockSsoSignIn).toHaveBeenCalledTimes(2)
+ })
+
+ it('does not expose the message from a rejected sign-in request', async () => {
+ mockSsoSignIn.mockRejectedValue(new Error('INVALID_EMAIL_DOMAIN'))
+ renderInteractive('email=user%40example.com')
+
+ await submitForm()
+
+ expect(container).toHaveTextContent('Unable to start SSO. Check your email and try again.')
+ expect(container).not.toHaveTextContent('INVALID_EMAIL_DOMAIN')
+ const submitButton = container.querySelector('button[type="submit"]')
+ expect(submitButton?.disabled).toBe(false)
+ expect(submitButton).toHaveTextContent('Continue with SSO')
+ })
+})
diff --git a/apps/sim/ee/sso/components/sso-form.tsx b/apps/sim/ee/sso/components/sso-form.tsx
index f2c3cd5f4c6..ca20d1db5e4 100644
--- a/apps/sim/ee/sso/components/sso-form.tsx
+++ b/apps/sim/ee/sso/components/sso-form.tsx
@@ -12,6 +12,7 @@ import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { AuthSubmitButton } from '@/app/(auth)/components'
const logger = createLogger('SSOForm')
+const SSO_SIGN_IN_ERROR = 'Unable to start SSO. Check your email and try again.'
const validateEmailField = (emailValue: string): string[] => {
const errors: string[] = []
@@ -110,33 +111,22 @@ export default function SSOForm({ registrationDisabled }: SSOFormProps) {
try {
const safeCallbackUrl = callbackUrl
- await client.signIn.sso({
+ const result = await client.signIn.sso({
email: emailValue,
callbackURL: safeCallbackUrl,
errorCallbackURL: `/sso?error=sso_failed&callbackUrl=${encodeURIComponent(safeCallbackUrl)}`,
})
- } catch (err) {
- logger.error('SSO sign-in failed', { error: err, email: emailValue })
- let errorMessage = 'SSO sign-in failed. Please try again.'
- if (err instanceof Error) {
- if (err.message.includes('NO_PROVIDER_FOUND')) {
- errorMessage = 'SSO provider not found. Please check your configuration.'
- } else if (err.message.includes('INVALID_EMAIL_DOMAIN')) {
- errorMessage = 'Email domain not configured for SSO. Please contact your administrator.'
- } else if (err.message.includes('network')) {
- errorMessage = 'Network error. Please check your connection and try again.'
- } else if (err.message.includes('rate limit')) {
- errorMessage = 'Too many requests. Please wait a moment before trying again.'
- } else if (err.message.includes('SSO_DISABLED')) {
- errorMessage = 'SSO authentication is disabled. Please use another sign-in method.'
- } else {
- errorMessage = err.message
- }
+ if (!result || result.error) {
+ logger.error('SSO sign-in failed', { error: result?.error, email: emailValue })
+ setEmailErrors([SSO_SIGN_IN_ERROR])
+ setShowEmailValidationError(true)
}
-
- setEmailErrors([errorMessage])
+ } catch (err) {
+ logger.error('SSO sign-in failed', { error: err, email: emailValue })
+ setEmailErrors([SSO_SIGN_IN_ERROR])
setShowEmailValidationError(true)
+ } finally {
setIsLoading(false)
}
}