From 6db7aa37ddc2ffc74b5ea47cab9a2fe543647730 Mon Sep 17 00:00:00 2001 From: msukkari Date: Sat, 1 Nov 2025 15:11:41 -0700 Subject: [PATCH] properly handle sso idps from config --- .../src/app/components/authMethodSelector.tsx | 10 +++---- packages/web/src/app/invite/page.tsx | 6 ++--- .../src/app/login/components/loginForm.tsx | 4 +-- packages/web/src/app/login/page.tsx | 4 +-- packages/web/src/app/onboard/page.tsx | 4 +-- packages/web/src/app/signup/page.tsx | 4 +-- packages/web/src/auth.ts | 23 +++++++++------- packages/web/src/ee/features/sso/sso.ts | 26 +++++++------------ packages/web/src/lib/authProviders.ts | 13 +++++----- 9 files changed, 47 insertions(+), 47 deletions(-) diff --git a/packages/web/src/app/components/authMethodSelector.tsx b/packages/web/src/app/components/authMethodSelector.tsx index 84d1228e..9d6f2734 100644 --- a/packages/web/src/app/components/authMethodSelector.tsx +++ b/packages/web/src/app/components/authMethodSelector.tsx @@ -8,10 +8,10 @@ import { CredentialsForm } from "@/app/login/components/credentialsForm"; import { DividerSet } from "@/app/components/dividerSet"; import { ProviderButton } from "@/app/components/providerButton"; import { AuthSecurityNotice } from "@/app/components/authSecurityNotice"; -import type { AuthProvider } from "@/lib/authProviders"; +import type { IdentityProviderMetadata } from "@/lib/authProviders"; interface AuthMethodSelectorProps { - providers: AuthProvider[]; + providers: IdentityProviderMetadata[]; callbackUrl?: string; context: "login" | "signup"; onProviderClick?: (providerId: string) => void; @@ -35,11 +35,11 @@ export const AuthMethodSelector = ({ }, [callbackUrl, onProviderClick]); // Separate OAuth providers from special auth methods - const oauthProviders = providers.filter(p => + const oauthProviders = providers.filter(p => p.purpose === "sso" && !["credentials", "nodemailer"].includes(p.id) ); - const hasCredentials = providers.some(p => p.id === "credentials"); - const hasMagicLink = providers.some(p => p.id === "nodemailer"); + const hasCredentials = providers.some(p => p.purpose === "sso" && p.id === "credentials"); + const hasMagicLink = providers.some(p => p.purpose === "sso" && p.id === "nodemailer"); return ( <> diff --git a/packages/web/src/app/invite/page.tsx b/packages/web/src/app/invite/page.tsx index 195a8d17..df284589 100644 --- a/packages/web/src/app/invite/page.tsx +++ b/packages/web/src/app/invite/page.tsx @@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { SourcebotLogo } from "@/app/components/sourcebotLogo"; import { AuthMethodSelector } from "@/app/components/authMethodSelector"; import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"; -import { getAuthProviders } from "@/lib/authProviders"; +import { getIdentityProviderMetadata, IdentityProviderMetadata } from "@/lib/authProviders"; import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard"; interface InvitePageProps { @@ -30,7 +30,7 @@ export default async function InvitePage(props: InvitePageProps) { const session = await auth(); if (!session) { - const providers = getAuthProviders(); + const providers = getIdentityProviderMetadata(); return ; } @@ -57,7 +57,7 @@ export default async function InvitePage(props: InvitePageProps) { ); } -function WelcomeCard({ inviteLinkId, providers }: { inviteLinkId: string; providers: import("@/lib/authProviders").AuthProvider[] }) { +function WelcomeCard({ inviteLinkId, providers }: { inviteLinkId: string; providers: IdentityProviderMetadata[] }) { return (
diff --git a/packages/web/src/app/login/components/loginForm.tsx b/packages/web/src/app/login/components/loginForm.tsx index 1d1eb5e3..f24bb8f5 100644 --- a/packages/web/src/app/login/components/loginForm.tsx +++ b/packages/web/src/app/login/components/loginForm.tsx @@ -6,12 +6,12 @@ import { SourcebotLogo } from "@/app/components/sourcebotLogo"; import { AuthMethodSelector } from "@/app/components/authMethodSelector"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import Link from "next/link"; -import type { AuthProvider } from "@/lib/authProviders"; +import type { IdentityProviderMetadata } from "@/lib/authProviders"; interface LoginFormProps { callbackUrl?: string; error?: string; - providers: AuthProvider[]; + providers: IdentityProviderMetadata[]; context: "login" | "signup"; } diff --git a/packages/web/src/app/login/page.tsx b/packages/web/src/app/login/page.tsx index 1535ec58..730ca610 100644 --- a/packages/web/src/app/login/page.tsx +++ b/packages/web/src/app/login/page.tsx @@ -2,7 +2,7 @@ import { auth } from "@/auth"; import { LoginForm } from "./components/loginForm"; import { redirect } from "next/navigation"; import { Footer } from "@/app/components/footer"; -import { getAuthProviders } from "@/lib/authProviders"; +import { getIdentityProviderMetadata } from "@/lib/authProviders"; import { getOrgFromDomain } from "@/data/org"; import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; @@ -25,7 +25,7 @@ export default async function Login(props: LoginProps) { return redirect("/onboard"); } - const providers = getAuthProviders(); + const providers = getIdentityProviderMetadata(); return (
diff --git a/packages/web/src/app/onboard/page.tsx b/packages/web/src/app/onboard/page.tsx index b446c15f..d995fa9c 100644 --- a/packages/web/src/app/onboard/page.tsx +++ b/packages/web/src/app/onboard/page.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button" import { AuthMethodSelector } from "@/app/components/authMethodSelector" import { SourcebotLogo } from "@/app/components/sourcebotLogo" import { auth } from "@/auth"; -import { getAuthProviders } from "@/lib/authProviders"; +import { getIdentityProviderMetadata } from "@/lib/authProviders"; import { OrganizationAccessSettings } from "@/app/components/organizationAccessSettings"; import { CompleteOnboardingButton } from "./components/completeOnboardingButton"; import { getOrgFromDomain } from "@/data/org"; @@ -41,7 +41,7 @@ interface ResourceCard { export default async function Onboarding(props: OnboardingProps) { const searchParams = await props.searchParams; - const providers = getAuthProviders(); + const providers = getIdentityProviderMetadata(); const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); const session = await auth(); diff --git a/packages/web/src/app/signup/page.tsx b/packages/web/src/app/signup/page.tsx index dc920596..8c2c76ff 100644 --- a/packages/web/src/app/signup/page.tsx +++ b/packages/web/src/app/signup/page.tsx @@ -3,7 +3,7 @@ import { LoginForm } from "../login/components/loginForm"; import { redirect } from "next/navigation"; import { Footer } from "@/app/components/footer"; import { createLogger } from "@sourcebot/logger"; -import { getAuthProviders } from "@/lib/authProviders"; +import { getIdentityProviderMetadata } from "@/lib/authProviders"; import { getOrgFromDomain } from "@/data/org"; import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; @@ -29,7 +29,7 @@ export default async function Signup(props: LoginProps) { return redirect("/onboard"); } - const providers = getAuthProviders(); + const providers = getIdentityProviderMetadata(); return (
diff --git a/packages/web/src/auth.ts b/packages/web/src/auth.ts index 544976d0..68a2501e 100644 --- a/packages/web/src/auth.ts +++ b/packages/web/src/auth.ts @@ -13,17 +13,22 @@ import { createTransport } from 'nodemailer'; import { render } from '@react-email/render'; import MagicLinkEmail from './emails/magicLinkEmail'; import bcrypt from 'bcryptjs'; -import { getSSOProviders } from '@/ee/features/sso/sso'; +import { getEEIdentityProviders } from '@/ee/features/sso/sso'; import { hasEntitlement } from '@sourcebot/shared'; import { onCreateUser } from '@/lib/authUtils'; import { getAuditService } from '@/ee/features/audit/factory'; import { SINGLE_TENANT_ORG_ID } from './lib/constants'; const auditService = getAuditService(); -const ssoProviders = hasEntitlement("sso") ? await getSSOProviders() : []; +const eeIdentityProviders = hasEntitlement("sso") ? await getEEIdentityProviders() : []; export const runtime = 'nodejs'; +export type IdentityProvider = { + provider: Provider; + purpose: "sso" | "integration"; +} + declare module 'next-auth' { interface Session { user: { @@ -33,16 +38,16 @@ declare module 'next-auth' { } declare module 'next-auth/jwt' { - interface JWT { + interface JWT { userId: string } } export const getProviders = () => { - const providers: Provider[] = ssoProviders; + const providers: IdentityProvider[] = eeIdentityProviders; if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS && env.AUTH_EMAIL_CODE_LOGIN_ENABLED === 'true') { - providers.push(EmailProvider({ + providers.push({ provider: EmailProvider({ server: env.SMTP_CONNECTION_URL, from: env.EMAIL_FROM_ADDRESS, maxAge: 60 * 10, @@ -66,11 +71,11 @@ export const getProviders = () => { throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`); } } - })); + }), purpose: "sso"}); } if (env.AUTH_CREDENTIALS_LOGIN_ENABLED === 'true') { - providers.push(Credentials({ + providers.push({ provider: Credentials({ credentials: { email: {}, password: {} @@ -123,7 +128,7 @@ export const getProviders = () => { }; } } - })); + }), purpose: "sso"}); } return providers; @@ -193,7 +198,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ return session; }, }, - providers: getProviders(), + providers: getProviders().map((provider) => provider.provider), pages: { signIn: "/login", // We set redirect to false in signInOptions so we can pass the email is as a param diff --git a/packages/web/src/ee/features/sso/sso.ts b/packages/web/src/ee/features/sso/sso.ts index 8a6d8856..55092032 100644 --- a/packages/web/src/ee/features/sso/sso.ts +++ b/packages/web/src/ee/features/sso/sso.ts @@ -1,4 +1,3 @@ -import type { Provider } from "next-auth/providers"; import { env } from "@/env.mjs"; import GitHub from "next-auth/providers/github"; import Google from "next-auth/providers/google"; @@ -14,12 +13,13 @@ import { onCreateUser } from "@/lib/authUtils"; import { createLogger } from "@sourcebot/logger"; import { hasEntitlement, loadConfig } from "@sourcebot/shared"; import { getTokenFromConfig } from "@sourcebot/crypto"; +import type { IdentityProvider } from "@/auth"; import { GCPIAPIdentityProviderConfig, GitHubIdentityProviderConfig, GitLabIdentityProviderConfig, GoogleIdentityProviderConfig, KeycloakIdentityProviderConfig, MicrosoftEntraIDIdentityProviderConfig, OktaIdentityProviderConfig } from "@sourcebot/schemas/v3/index.type"; const logger = createLogger('web-sso'); -export const getSSOProviders = async (): Promise => { - const providers: Provider[] = []; +export const getEEIdentityProviders = async (): Promise => { + const providers: IdentityProvider[] = []; const config = env.CONFIG_PATH ? await loadConfig(env.CONFIG_PATH) : undefined; const identityProviders = config?.identityProviders ?? []; @@ -27,55 +27,49 @@ export const getSSOProviders = async (): Promise => { for (const identityProvider of identityProviders) { if (identityProvider.provider === "github") { const providerConfig = identityProvider as GitHubIdentityProviderConfig; - if (providerConfig.purpose !== "sso") { - continue; - } const clientId = await getTokenFromConfig(providerConfig.clientId); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const baseUrl = providerConfig.baseUrl ? await getTokenFromConfig(providerConfig.baseUrl) : undefined; - providers.push(createGitHubProvider(clientId, clientSecret, baseUrl)); + providers.push({ provider: createGitHubProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose }); } if (identityProvider.provider === "gitlab") { const providerConfig = identityProvider as GitLabIdentityProviderConfig; - if (providerConfig.purpose !== "sso") { - continue; - } const clientId = await getTokenFromConfig(providerConfig.clientId); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const baseUrl = providerConfig.baseUrl ? await getTokenFromConfig(providerConfig.baseUrl) : undefined; - providers.push(createGitLabProvider(clientId, clientSecret, baseUrl)); + providers.push({ provider: createGitLabProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose }); } if (identityProvider.provider === "google") { const providerConfig = identityProvider as GoogleIdentityProviderConfig; const clientId = await getTokenFromConfig(providerConfig.clientId); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); - providers.push(createGoogleProvider(clientId, clientSecret)); + providers.push({ provider: createGoogleProvider(clientId, clientSecret), purpose: "sso"}); } if (identityProvider.provider === "okta") { const providerConfig = identityProvider as OktaIdentityProviderConfig; const clientId = await getTokenFromConfig(providerConfig.clientId); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const issuer = await getTokenFromConfig(providerConfig.issuer); - providers.push(createOktaProvider(clientId, clientSecret, issuer)); + providers.push({ provider: createOktaProvider(clientId, clientSecret, issuer), purpose: "sso"}); } if (identityProvider.provider === "keycloak") { const providerConfig = identityProvider as KeycloakIdentityProviderConfig; const clientId = await getTokenFromConfig(providerConfig.clientId); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const issuer = await getTokenFromConfig(providerConfig.issuer); - providers.push(createKeycloakProvider(clientId, clientSecret, issuer)); + providers.push({ provider: createKeycloakProvider(clientId, clientSecret, issuer), purpose: "sso"}); } if (identityProvider.provider === "microsoft-entra-id") { const providerConfig = identityProvider as MicrosoftEntraIDIdentityProviderConfig; const clientId = await getTokenFromConfig(providerConfig.clientId); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const issuer = await getTokenFromConfig(providerConfig.issuer); - providers.push(createMicrosoftEntraIDProvider(clientId, clientSecret, issuer)); + providers.push({ provider: createMicrosoftEntraIDProvider(clientId, clientSecret, issuer), purpose: "sso"}); } if (identityProvider.provider === "gcp-iap") { const providerConfig = identityProvider as GCPIAPIdentityProviderConfig; const audience = await getTokenFromConfig(providerConfig.audience); - providers.push(createGCPIAPProvider(audience)); + providers.push({ provider: createGCPIAPProvider(audience), purpose: "sso"}); } } diff --git a/packages/web/src/lib/authProviders.ts b/packages/web/src/lib/authProviders.ts index ca2a6697..efcb54ff 100644 --- a/packages/web/src/lib/authProviders.ts +++ b/packages/web/src/lib/authProviders.ts @@ -1,18 +1,19 @@ import { getProviders } from "@/auth"; -export interface AuthProvider { +export interface IdentityProviderMetadata { id: string; name: string; + purpose: "sso" | "integration"; } -export const getAuthProviders = (): AuthProvider[] => { +export const getIdentityProviderMetadata = (): IdentityProviderMetadata[] => { const providers = getProviders(); return providers.map((provider) => { - if (typeof provider === "function") { - const providerInfo = provider(); - return { id: providerInfo.id, name: providerInfo.name }; + if (typeof provider.provider === "function") { + const providerInfo = provider.provider(); + return { id: providerInfo.id, name: providerInfo.name, purpose: provider.purpose }; } else { - return { id: provider.id, name: provider.name }; + return { id: provider.provider.id, name: provider.provider.name, purpose: provider.purpose }; } }); }; \ No newline at end of file