Skip to content

Commit 7d796b3

Browse files
committed
feat(auth): port OAuth-only signup + Microsoft provider from staging
Align auth-page logic with origin/staging (PR #5073) while keeping the new chip-styled UI: - Add Microsoft as a better-auth social sign-in provider (auth.ts) and surface it through the OAuth provider checker, providers API + contract, login/signup forms, SocialLoginButtons, and the landing auth modal. - Gate email/password signup behind the emailSignupEnabled server flag (DISABLE_EMAIL_SIGNUP) so signup becomes OAuth-only when configured. - Add DISABLE_MICROSOFT_AUTH / DISABLE_EMAIL_SIGNUP env + feature flags.
1 parent 8e1ca77 commit 7d796b3

12 files changed

Lines changed: 122 additions & 18 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: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { type ReactNode, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { getErrorMessage } from '@sim/utils/errors'
66
import { Chip } from '@/components/emcn'
7-
import { GithubIcon, GoogleIcon } from '@/components/icons'
7+
import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons'
88
import { client } from '@/lib/auth/auth-client'
99
import { cn } from '@/lib/core/utils/cn'
1010
import { AUTH_BUTTON_CLASS } from '@/app/(auth)/components/constants'
@@ -14,6 +14,7 @@ const logger = createLogger('SocialLoginButtons')
1414
interface SocialLoginButtonsProps {
1515
githubAvailable: boolean
1616
googleAvailable: boolean
17+
microsoftAvailable: boolean
1718
callbackURL?: string
1819
isProduction: boolean
1920
children?: ReactNode
@@ -22,12 +23,14 @@ interface SocialLoginButtonsProps {
2223
export function SocialLoginButtons({
2324
githubAvailable,
2425
googleAvailable,
26+
microsoftAvailable,
2527
callbackURL = '/workspace',
2628
isProduction,
2729
children,
2830
}: SocialLoginButtonsProps) {
2931
const [isGithubLoading, setIsGithubLoading] = useState(false)
3032
const [isGoogleLoading, setIsGoogleLoading] = useState(false)
33+
const [isMicrosoftLoading, setIsMicrosoftLoading] = useState(false)
3134

3235
async function signInWithGithub() {
3336
if (!githubAvailable) return
@@ -55,6 +58,19 @@ export function SocialLoginButtons({
5558
}
5659
}
5760

61+
async function signInWithMicrosoft() {
62+
if (!microsoftAvailable) return
63+
64+
setIsMicrosoftLoading(true)
65+
try {
66+
await client.signIn.social({ provider: 'microsoft', callbackURL })
67+
} catch (err) {
68+
logger.error('Microsoft sign-in failed', { error: getErrorMessage(err) })
69+
} finally {
70+
setIsMicrosoftLoading(false)
71+
}
72+
}
73+
5874
const githubButton = (
5975
<Chip
6076
fullWidth
@@ -81,7 +97,20 @@ export function SocialLoginButtons({
8197
</Chip>
8298
)
8399

84-
const hasAnyOAuthProvider = githubAvailable || googleAvailable
100+
const microsoftButton = (
101+
<Chip
102+
fullWidth
103+
flush
104+
leftIcon={MicrosoftIcon}
105+
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
106+
disabled={!microsoftAvailable || isMicrosoftLoading}
107+
onClick={signInWithMicrosoft}
108+
>
109+
{isMicrosoftLoading ? 'Connecting…' : 'Microsoft'}
110+
</Chip>
111+
)
112+
113+
const hasAnyOAuthProvider = githubAvailable || googleAvailable || microsoftAvailable
85114

86115
if (!hasAnyOAuthProvider && !children) {
87116
return null
@@ -90,6 +119,7 @@ export function SocialLoginButtons({
90119
return (
91120
<div className='grid gap-3'>
92121
{googleAvailable && googleButton}
122+
{microsoftAvailable && microsoftButton}
93123
{githubAvailable && githubButton}
94124
{children}
95125
</div>

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,12 @@ const validatePassword = (passwordValue: string): string[] => {
8383
export default function LoginPage({
8484
githubAvailable,
8585
googleAvailable,
86+
microsoftAvailable,
8687
isProduction,
8788
}: {
8889
githubAvailable: boolean
8990
googleAvailable: boolean
91+
microsoftAvailable: boolean
9092
isProduction: boolean
9193
}) {
9294
const router = useRouter()
@@ -337,7 +339,7 @@ export default function LoginPage({
337339

338340
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
339341
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
340-
const hasSocial = githubAvailable || googleAvailable
342+
const hasSocial = githubAvailable || googleAvailable || microsoftAvailable
341343
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
342344
const showTopSSO = hasOnlySSO
343345
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
@@ -417,6 +419,7 @@ export default function LoginPage({
417419
<SocialLoginButtons
418420
googleAvailable={googleAvailable}
419421
githubAvailable={githubAvailable}
422+
microsoftAvailable={microsoftAvailable}
420423
isProduction={isProduction}
421424
callbackURL={callbackUrl}
422425
>

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: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,18 @@ const validateEmailField = (emailValue: string): string[] => {
8181
interface SignupFormProps {
8282
githubAvailable: boolean
8383
googleAvailable: boolean
84+
microsoftAvailable: boolean
8485
isProduction: boolean
86+
emailSignupEnabled: boolean
8587
}
8688

87-
function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: SignupFormProps) {
89+
function SignupFormContent({
90+
githubAvailable,
91+
googleAvailable,
92+
microsoftAvailable,
93+
isProduction,
94+
emailSignupEnabled,
95+
}: SignupFormProps) {
8896
const router = useRouter()
8997
const searchParams = useSearchParams()
9098
const { refetch: refetchSession } = useSession()
@@ -352,8 +360,9 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
352360
}
353361

354362
const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
355-
const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
356-
const hasSocial = githubAvailable || googleAvailable
363+
const emailEnabled =
364+
!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && emailSignupEnabled
365+
const hasSocial = githubAvailable || googleAvailable || microsoftAvailable
357366
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
358367
const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO)
359368
const showDivider = (emailEnabled || hasOnlySSO) && showBottomSection
@@ -450,10 +459,11 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
450459
<SocialLoginButtons
451460
githubAvailable={githubAvailable}
452461
googleAvailable={googleAvailable}
462+
microsoftAvailable={microsoftAvailable}
453463
callbackURL={redirectUrl || '/workspace'}
454464
isProduction={isProduction}
455465
>
456-
{ssoEnabled && (
466+
{ssoEnabled && !hasOnlySSO && (
457467
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='outline' />
458468
)}
459469
</SocialLoginButtons>
@@ -473,14 +483,18 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
473483
export default function SignupPage({
474484
githubAvailable,
475485
googleAvailable,
486+
microsoftAvailable,
476487
isProduction,
488+
emailSignupEnabled,
477489
}: SignupFormProps) {
478490
return (
479491
<Suspense fallback={<div className='flex h-screen items-center justify-center'>Loading…</div>}>
480492
<SignupFormContent
481493
githubAvailable={githubAvailable}
482494
googleAvailable={googleAvailable}
495+
microsoftAvailable={microsoftAvailable}
483496
isProduction={isProduction}
497+
emailSignupEnabled={emailSignupEnabled}
484498
/>
485499
</Suspense>
486500
)

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

Lines changed: 23 additions & 5 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' })
@@ -181,6 +186,19 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
181186
</span>
182187
</button>
183188
)}
189+
{providerStatus.microsoftAvailable && (
190+
<button
191+
type='button'
192+
onClick={() => handleSocialLogin('microsoft')}
193+
disabled={!!socialLoading}
194+
className={SOCIAL_BTN}
195+
>
196+
<MicrosoftIcon className='absolute left-4 size-[18px] shrink-0' />
197+
<span>
198+
{socialLoading === 'microsoft' ? 'Connecting...' : 'Continue with Microsoft'}
199+
</span>
200+
</button>
201+
)}
184202
{providerStatus.githubAvailable && (
185203
<button
186204
type='button'

apps/sim/app/api/auth/providers/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
1212
const parsed = await parseRequest(getAuthProvidersContract, request, {})
1313
if (!parsed.success) return parsed.response
1414

15-
const { githubAvailable, googleAvailable } = await getOAuthProviderStatus()
15+
const { githubAvailable, googleAvailable, microsoftAvailable } = await getOAuthProviderStatus()
1616
return NextResponse.json({
1717
githubAvailable,
1818
googleAvailable,
19+
microsoftAvailable,
1920
registrationDisabled: isRegistrationDisabled,
2021
})
2122
})

apps/sim/lib/api/contracts/auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const ssoProvidersQuerySchema = z.object({
99
export const authProviderStatusResponseSchema = z.object({
1010
githubAvailable: z.boolean(),
1111
googleAvailable: z.boolean(),
12+
microsoftAvailable: z.boolean(),
1213
registrationDisabled: z.boolean(),
1314
})
1415

apps/sim/lib/auth/auth.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
isGithubAuthDisabled,
7070
isGoogleAuthDisabled,
7171
isHosted,
72+
isMicrosoftAuthDisabled,
7273
isOrganizationsEnabled,
7374
isRegistrationDisabled,
7475
isSignupEmailValidationEnabled,
@@ -724,6 +725,15 @@ export const auth = betterAuth({
724725
],
725726
},
726727
}),
728+
...(!isMicrosoftAuthDisabled &&
729+
env.MICROSOFT_CLIENT_ID &&
730+
env.MICROSOFT_CLIENT_SECRET && {
731+
microsoft: {
732+
clientId: env.MICROSOFT_CLIENT_ID,
733+
clientSecret: env.MICROSOFT_CLIENT_SECRET,
734+
scope: ['openid', 'profile', 'email'],
735+
},
736+
}),
727737
},
728738
emailVerification: {
729739
autoSignInAfterVerification: true,

0 commit comments

Comments
 (0)