Skip to content

Commit a49e755

Browse files
feat(auth): OAuth-only signup with Microsoft provider (#5073)
* feat(auth): OAuth-only signup with Microsoft provider - Remove email/password form from /signup — Google, Microsoft, GitHub OAuth only - Add Microsoft as a social provider (MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET / DISABLE_MICROSOFT_AUTH) - Wire microsoftAvailable through provider checker, API contract, providers route, and all auth UI - Hide "Continue with email" in auth modal signup view; login view unchanged - Fix MicrosoftIcon SVG to use official brand colors and proportions * fix(auth): remove unused useSession, guard invalid-callback warn with ref * feat(auth): gate email signup via DISABLE_EMAIL_SIGNUP flag * feat(auth): restore signup email form gated by NEXT_PUBLIC_DISABLE_EMAIL_SIGNUP * refactor(auth): single DISABLE_EMAIL_SIGNUP env var controls both ui and backend * fix(config): restore isHosted hostname check
1 parent 4f4ff53 commit a49e755

13 files changed

Lines changed: 155 additions & 56 deletions

File tree

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { env } from '@/lib/core/config/env'
2-
import { isGithubAuthDisabled, isGoogleAuthDisabled, isProd } from '@/lib/core/config/feature-flags'
2+
import {
3+
isGithubAuthDisabled,
4+
isGoogleAuthDisabled,
5+
isMicrosoftAuthDisabled,
6+
isProd,
7+
} from '@/lib/core/config/feature-flags'
38

49
export async function getOAuthProviderStatus() {
510
const githubAvailable =
@@ -8,5 +13,8 @@ export async function getOAuthProviderStatus() {
813
const googleAvailable =
914
!!(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) && !isGoogleAuthDisabled
1015

11-
return { githubAvailable, googleAvailable, isProduction: isProd }
16+
const microsoftAvailable =
17+
!!(env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) && !isMicrosoftAuthDisabled
18+
19+
return { githubAvailable, googleAvailable, microsoftAvailable, isProduction: isProd }
1220
}

apps/sim/app/(auth)/components/social-login-buttons.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
import { type ReactNode, useState } from 'react'
44
import { Button } from '@/components/emcn'
5-
import { GithubIcon, GoogleIcon } from '@/components/icons'
5+
import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons'
66
import { client } from '@/lib/auth/auth-client'
77

88
interface SocialLoginButtonsProps {
99
githubAvailable: boolean
1010
googleAvailable: boolean
11+
microsoftAvailable: boolean
1112
callbackURL?: string
1213
isProduction: boolean
1314
children?: ReactNode
@@ -16,12 +17,14 @@ interface SocialLoginButtonsProps {
1617
export function SocialLoginButtons({
1718
githubAvailable,
1819
googleAvailable,
20+
microsoftAvailable,
1921
callbackURL = '/workspace',
2022
isProduction,
2123
children,
2224
}: SocialLoginButtonsProps) {
2325
const [isGithubLoading, setIsGithubLoading] = useState(false)
2426
const [isGoogleLoading, setIsGoogleLoading] = useState(false)
27+
const [isMicrosoftLoading, setIsMicrosoftLoading] = useState(false)
2528

2629
async function signInWithGithub() {
2730
if (!githubAvailable) return
@@ -69,6 +72,29 @@ export function SocialLoginButtons({
6972
}
7073
}
7174

75+
async function signInWithMicrosoft() {
76+
if (!microsoftAvailable) return
77+
78+
setIsMicrosoftLoading(true)
79+
try {
80+
await client.signIn.social({ provider: 'microsoft', callbackURL })
81+
} catch (err: any) {
82+
let errorMessage = 'Failed to sign in with Microsoft'
83+
84+
if (err.message?.includes('account exists')) {
85+
errorMessage = 'An account with this email already exists. Please sign in instead.'
86+
} else if (err.message?.includes('cancelled')) {
87+
errorMessage = 'Microsoft sign in was cancelled. Please try again.'
88+
} else if (err.message?.includes('network')) {
89+
errorMessage = 'Network error. Please check your connection and try again.'
90+
} else if (err.message?.includes('rate limit')) {
91+
errorMessage = 'Too many attempts. Please try again later.'
92+
}
93+
} finally {
94+
setIsMicrosoftLoading(false)
95+
}
96+
}
97+
7298
const githubButton = (
7399
<Button
74100
variant='outline'
@@ -93,7 +119,19 @@ export function SocialLoginButtons({
93119
</Button>
94120
)
95121

96-
const hasAnyOAuthProvider = githubAvailable || googleAvailable
122+
const microsoftButton = (
123+
<Button
124+
variant='outline'
125+
className='w-full rounded-sm border-[var(--landing-border-strong)] py-1.5 text-sm'
126+
disabled={!microsoftAvailable || isMicrosoftLoading}
127+
onClick={signInWithMicrosoft}
128+
>
129+
<MicrosoftIcon className='!h-[18px] !w-[18px] mr-1' />
130+
{isMicrosoftLoading ? 'Connecting...' : 'Microsoft'}
131+
</Button>
132+
)
133+
134+
const hasAnyOAuthProvider = githubAvailable || googleAvailable || microsoftAvailable
97135

98136
if (!hasAnyOAuthProvider && !children) {
99137
return null
@@ -102,6 +140,7 @@ export function SocialLoginButtons({
102140
return (
103141
<div className='grid gap-3 font-light'>
104142
{googleAvailable && googleButton}
143+
{microsoftAvailable && microsoftButton}
105144
{githubAvailable && githubButton}
106145
{children}
107146
</div>

apps/sim/app/(auth)/login/login-form.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,12 @@ const validatePassword = (passwordValue: string): string[] => {
7878
export default function LoginPage({
7979
githubAvailable,
8080
googleAvailable,
81+
microsoftAvailable,
8182
isProduction,
8283
}: {
8384
githubAvailable: boolean
8485
googleAvailable: boolean
86+
microsoftAvailable: boolean
8587
isProduction: boolean
8688
}) {
8789
const router = useRouter()
@@ -335,7 +337,7 @@ export default function LoginPage({
335337

336338
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
337339
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
338-
const hasSocial = githubAvailable || googleAvailable
340+
const hasSocial = githubAvailable || googleAvailable || microsoftAvailable
339341
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
340342
const showTopSSO = hasOnlySSO
341343
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
@@ -483,6 +485,7 @@ export default function LoginPage({
483485
<div className={cn(!emailEnabled ? 'mt-8' : undefined)}>
484486
<SocialLoginButtons
485487
googleAvailable={googleAvailable}
488+
microsoftAvailable={microsoftAvailable}
486489
githubAvailable={githubAvailable}
487490
isProduction={isProduction}
488491
callbackURL={callbackUrl}

apps/sim/app/(auth)/login/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ export const metadata: Metadata = {
1010
export const dynamic = 'force-dynamic'
1111

1212
export default async function LoginPage() {
13-
const { githubAvailable, googleAvailable, isProduction } = await getOAuthProviderStatus()
13+
const { githubAvailable, googleAvailable, microsoftAvailable, isProduction } =
14+
await getOAuthProviderStatus()
1415

1516
return (
1617
<Suspense fallback={null}>
1718
<LoginForm
1819
githubAvailable={githubAvailable}
1920
googleAvailable={googleAvailable}
21+
microsoftAvailable={microsoftAvailable}
2022
isProduction={isProduction}
2123
/>
2224
</Suspense>

apps/sim/app/(auth)/signup/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Metadata } from 'next'
2-
import { isRegistrationDisabled } from '@/lib/core/config/feature-flags'
2+
import { isEmailSignupDisabled, isRegistrationDisabled } from '@/lib/core/config/feature-flags'
33
import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker'
44
import SignupForm from '@/app/(auth)/signup/signup-form'
55

@@ -14,13 +14,16 @@ export default async function SignupPage() {
1414
return <div>Registration is disabled, please contact your admin.</div>
1515
}
1616

17-
const { githubAvailable, googleAvailable, isProduction } = await getOAuthProviderStatus()
17+
const { githubAvailable, googleAvailable, microsoftAvailable, isProduction } =
18+
await getOAuthProviderStatus()
1819

1920
return (
2021
<SignupForm
2122
githubAvailable={githubAvailable}
2223
googleAvailable={googleAvailable}
24+
microsoftAvailable={microsoftAvailable}
2325
isProduction={isProduction}
26+
emailSignupEnabled={!isEmailSignupDisabled}
2427
/>
2528
)
2629
}

apps/sim/app/(auth)/signup/signup-form.tsx

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,18 @@ const validateEmailField = (emailValue: string): string[] => {
7575
interface SignupFormProps {
7676
githubAvailable: boolean
7777
googleAvailable: boolean
78+
microsoftAvailable: boolean
7879
isProduction: boolean
80+
emailSignupEnabled: boolean
7981
}
8082

81-
function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: SignupFormProps) {
83+
function SignupFormContent({
84+
githubAvailable,
85+
googleAvailable,
86+
microsoftAvailable,
87+
isProduction,
88+
emailSignupEnabled,
89+
}: SignupFormProps) {
8290
const router = useRouter()
8391
const searchParams = useSearchParams()
8492
const { refetch: refetchSession } = useSession()
@@ -346,6 +354,14 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
346354
}
347355
}
348356

357+
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
358+
const emailEnabled =
359+
!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && emailSignupEnabled
360+
const hasSocial = githubAvailable || googleAvailable || microsoftAvailable
361+
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
362+
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
363+
const showDivider = (emailEnabled || hasOnlySSO) && showBottomSection
364+
349365
return (
350366
<>
351367
<div className='space-y-1 text-center'>
@@ -357,21 +373,13 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
357373
</p>
358374
</div>
359375

360-
{/* SSO Login Button (primary top-only when it is the only method) */}
361-
{(() => {
362-
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
363-
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
364-
const hasSocial = githubAvailable || googleAvailable
365-
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
366-
return hasOnlySSO
367-
})() && (
376+
{hasOnlySSO && (
368377
<div className='mt-8'>
369378
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='primary' />
370379
</div>
371380
)}
372381

373-
{/* Email/Password Form - show unless explicitly disabled */}
374-
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
382+
{emailEnabled && (
375383
<form onSubmit={onSubmit} className='mt-8 space-y-10'>
376384
<div className='space-y-6'>
377385
<div className='space-y-2'>
@@ -540,16 +548,7 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
540548
</form>
541549
)}
542550

543-
{/* Divider - show when we have multiple auth methods */}
544-
{(() => {
545-
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
546-
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
547-
const hasSocial = githubAvailable || googleAvailable
548-
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
549-
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
550-
const showDivider = (emailEnabled || hasOnlySSO) && showBottomSection
551-
return showDivider
552-
})() && (
551+
{showDivider && (
553552
<div className='relative my-6 font-light'>
554553
<div className='absolute inset-0 flex items-center'>
555554
<div className='w-full border-[var(--landing-bg-elevated)] border-t' />
@@ -562,26 +561,16 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
562561
</div>
563562
)}
564563

565-
{(() => {
566-
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
567-
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
568-
const hasSocial = githubAvailable || googleAvailable
569-
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
570-
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
571-
return showBottomSection
572-
})() && (
573-
<div
574-
className={cn(
575-
isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) ? 'mt-8' : undefined
576-
)}
577-
>
564+
{showBottomSection && (
565+
<div className={cn(!emailEnabled ? 'mt-8' : undefined)}>
578566
<SocialLoginButtons
579567
githubAvailable={githubAvailable}
580568
googleAvailable={googleAvailable}
569+
microsoftAvailable={microsoftAvailable}
581570
callbackURL={redirectUrl || '/workspace'}
582571
isProduction={isProduction}
583572
>
584-
{isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) && (
573+
{ssoEnabled && !hasOnlySSO && (
585574
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='outline' />
586575
)}
587576
</SocialLoginButtons>
@@ -625,14 +614,18 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
625614
export default function SignupPage({
626615
githubAvailable,
627616
googleAvailable,
617+
microsoftAvailable,
628618
isProduction,
619+
emailSignupEnabled,
629620
}: SignupFormProps) {
630621
return (
631622
<Suspense fallback={<div className='flex h-screen items-center justify-center'>Loading…</div>}>
632623
<SignupFormContent
633624
githubAvailable={githubAvailable}
634625
googleAvailable={googleAvailable}
626+
microsoftAvailable={microsoftAvailable}
635627
isProduction={isProduction}
628+
emailSignupEnabled={emailSignupEnabled}
636629
/>
637630
</Suspense>
638631
)

apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
ModalTitle,
1515
ModalTrigger,
1616
} from '@/components/emcn'
17-
import { GithubIcon, GoogleIcon } from '@/components/icons'
17+
import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons'
1818
import { requestJson } from '@/lib/api/client/request'
1919
import { type AuthProviderStatusResponse, getAuthProvidersContract } from '@/lib/api/contracts/auth'
2020
import { client } from '@/lib/auth/auth-client'
@@ -40,6 +40,7 @@ let fetchPromise: Promise<AuthProviderStatusResponse> | null = null
4040
const FALLBACK_STATUS: ProviderStatus = {
4141
githubAvailable: false,
4242
googleAvailable: false,
43+
microsoftAvailable: false,
4344
registrationDisabled: false,
4445
}
4546

@@ -49,9 +50,10 @@ const SOCIAL_BTN =
4950
function fetchProviderStatus(): Promise<ProviderStatus> {
5051
if (fetchPromise) return fetchPromise
5152
fetchPromise = requestJson(getAuthProvidersContract, {})
52-
.then(({ githubAvailable, googleAvailable, registrationDisabled }) => ({
53+
.then(({ githubAvailable, googleAvailable, microsoftAvailable, registrationDisabled }) => ({
5354
githubAvailable,
5455
googleAvailable,
56+
microsoftAvailable,
5557
registrationDisabled,
5658
}))
5759
.catch(() => {
@@ -66,14 +68,17 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
6668
const [open, setOpen] = useState(false)
6769
const [view, setView] = useState<AuthView>(defaultView)
6870
const [providerStatus, setProviderStatus] = useState<ProviderStatus | null>(null)
69-
const [socialLoading, setSocialLoading] = useState<'github' | 'google' | null>(null)
71+
const [socialLoading, setSocialLoading] = useState<'github' | 'google' | 'microsoft' | null>(null)
7072
const brand = useMemo(() => getBrandConfig(), [])
7173

7274
useEffect(() => {
7375
fetchProviderStatus().then(setProviderStatus)
7476
}, [])
7577

76-
const hasSocial = providerStatus?.githubAvailable || providerStatus?.googleAvailable
78+
const hasSocial =
79+
providerStatus?.githubAvailable ||
80+
providerStatus?.googleAvailable ||
81+
providerStatus?.microsoftAvailable
7782
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
7883
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
7984
const hasModalContent = hasSocial || ssoEnabled
@@ -104,7 +109,7 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
104109
}
105110
}
106111

107-
async function handleSocialLogin(provider: 'github' | 'google') {
112+
async function handleSocialLogin(provider: 'github' | 'google' | 'microsoft') {
108113
setSocialLoading(provider)
109114
try {
110115
await client.signIn.social({ provider, callbackURL: '/workspace' })
@@ -184,6 +189,19 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
184189
</span>
185190
</button>
186191
)}
192+
{providerStatus.microsoftAvailable && (
193+
<button
194+
type='button'
195+
onClick={() => handleSocialLogin('microsoft')}
196+
disabled={!!socialLoading}
197+
className={SOCIAL_BTN}
198+
>
199+
<MicrosoftIcon className='absolute left-4 size-[18px] shrink-0' />
200+
<span>
201+
{socialLoading === 'microsoft' ? 'Connecting...' : 'Continue with Microsoft'}
202+
</span>
203+
</button>
204+
)}
187205
{providerStatus.githubAvailable && (
188206
<button
189207
type='button'
@@ -204,7 +222,8 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
204222
)}
205223
</div>
206224

207-
{emailEnabled && (
225+
{/* Email option only available on login — signup is OAuth-only */}
226+
{emailEnabled && view === 'login' && (
208227
<>
209228
<div className='relative my-4'>
210229
<div className='absolute inset-0 flex items-center'>

0 commit comments

Comments
 (0)