properly handle sso idps from config

This commit is contained in:
msukkari 2025-11-01 15:11:41 -07:00
parent e047eb06b9
commit 6db7aa37dd
9 changed files with 47 additions and 47 deletions

View file

@ -8,10 +8,10 @@ import { CredentialsForm } from "@/app/login/components/credentialsForm";
import { DividerSet } from "@/app/components/dividerSet"; import { DividerSet } from "@/app/components/dividerSet";
import { ProviderButton } from "@/app/components/providerButton"; import { ProviderButton } from "@/app/components/providerButton";
import { AuthSecurityNotice } from "@/app/components/authSecurityNotice"; import { AuthSecurityNotice } from "@/app/components/authSecurityNotice";
import type { AuthProvider } from "@/lib/authProviders"; import type { IdentityProviderMetadata } from "@/lib/authProviders";
interface AuthMethodSelectorProps { interface AuthMethodSelectorProps {
providers: AuthProvider[]; providers: IdentityProviderMetadata[];
callbackUrl?: string; callbackUrl?: string;
context: "login" | "signup"; context: "login" | "signup";
onProviderClick?: (providerId: string) => void; onProviderClick?: (providerId: string) => void;
@ -35,11 +35,11 @@ export const AuthMethodSelector = ({
}, [callbackUrl, onProviderClick]); }, [callbackUrl, onProviderClick]);
// Separate OAuth providers from special auth methods // 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) !["credentials", "nodemailer"].includes(p.id)
); );
const hasCredentials = providers.some(p => p.id === "credentials"); const hasCredentials = providers.some(p => p.purpose === "sso" && p.id === "credentials");
const hasMagicLink = providers.some(p => p.id === "nodemailer"); const hasMagicLink = providers.some(p => p.purpose === "sso" && p.id === "nodemailer");
return ( return (
<> <>

View file

@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { SourcebotLogo } from "@/app/components/sourcebotLogo"; import { SourcebotLogo } from "@/app/components/sourcebotLogo";
import { AuthMethodSelector } from "@/app/components/authMethodSelector"; import { AuthMethodSelector } from "@/app/components/authMethodSelector";
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"; import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
import { getAuthProviders } from "@/lib/authProviders"; import { getIdentityProviderMetadata, IdentityProviderMetadata } from "@/lib/authProviders";
import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard"; import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard";
interface InvitePageProps { interface InvitePageProps {
@ -30,7 +30,7 @@ export default async function InvitePage(props: InvitePageProps) {
const session = await auth(); const session = await auth();
if (!session) { if (!session) {
const providers = getAuthProviders(); const providers = getIdentityProviderMetadata();
return <WelcomeCard inviteLinkId={inviteLinkId} providers={providers} />; return <WelcomeCard inviteLinkId={inviteLinkId} providers={providers} />;
} }
@ -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 ( return (
<div className="min-h-screen bg-gradient-to-br from-[var(--background)] to-[var(--accent)]/30 flex items-center justify-center p-6"> <div className="min-h-screen bg-gradient-to-br from-[var(--background)] to-[var(--accent)]/30 flex items-center justify-center p-6">
<Card className="w-full max-w-md"> <Card className="w-full max-w-md">

View file

@ -6,12 +6,12 @@ import { SourcebotLogo } from "@/app/components/sourcebotLogo";
import { AuthMethodSelector } from "@/app/components/authMethodSelector"; import { AuthMethodSelector } from "@/app/components/authMethodSelector";
import useCaptureEvent from "@/hooks/useCaptureEvent"; import useCaptureEvent from "@/hooks/useCaptureEvent";
import Link from "next/link"; import Link from "next/link";
import type { AuthProvider } from "@/lib/authProviders"; import type { IdentityProviderMetadata } from "@/lib/authProviders";
interface LoginFormProps { interface LoginFormProps {
callbackUrl?: string; callbackUrl?: string;
error?: string; error?: string;
providers: AuthProvider[]; providers: IdentityProviderMetadata[];
context: "login" | "signup"; context: "login" | "signup";
} }

View file

@ -2,7 +2,7 @@ import { auth } from "@/auth";
import { LoginForm } from "./components/loginForm"; import { LoginForm } from "./components/loginForm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { Footer } from "@/app/components/footer"; import { Footer } from "@/app/components/footer";
import { getAuthProviders } from "@/lib/authProviders"; import { getIdentityProviderMetadata } from "@/lib/authProviders";
import { getOrgFromDomain } from "@/data/org"; import { getOrgFromDomain } from "@/data/org";
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
@ -25,7 +25,7 @@ export default async function Login(props: LoginProps) {
return redirect("/onboard"); return redirect("/onboard");
} }
const providers = getAuthProviders(); const providers = getIdentityProviderMetadata();
return ( return (
<div className="flex flex-col min-h-screen bg-backgroundSecondary"> <div className="flex flex-col min-h-screen bg-backgroundSecondary">
<div className="flex-1 flex flex-col items-center p-4 sm:p-12 w-full"> <div className="flex-1 flex flex-col items-center p-4 sm:p-12 w-full">

View file

@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"
import { AuthMethodSelector } from "@/app/components/authMethodSelector" import { AuthMethodSelector } from "@/app/components/authMethodSelector"
import { SourcebotLogo } from "@/app/components/sourcebotLogo" import { SourcebotLogo } from "@/app/components/sourcebotLogo"
import { auth } from "@/auth"; import { auth } from "@/auth";
import { getAuthProviders } from "@/lib/authProviders"; import { getIdentityProviderMetadata } from "@/lib/authProviders";
import { OrganizationAccessSettings } from "@/app/components/organizationAccessSettings"; import { OrganizationAccessSettings } from "@/app/components/organizationAccessSettings";
import { CompleteOnboardingButton } from "./components/completeOnboardingButton"; import { CompleteOnboardingButton } from "./components/completeOnboardingButton";
import { getOrgFromDomain } from "@/data/org"; import { getOrgFromDomain } from "@/data/org";
@ -41,7 +41,7 @@ interface ResourceCard {
export default async function Onboarding(props: OnboardingProps) { export default async function Onboarding(props: OnboardingProps) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const providers = getAuthProviders(); const providers = getIdentityProviderMetadata();
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
const session = await auth(); const session = await auth();

View file

@ -3,7 +3,7 @@ import { LoginForm } from "../login/components/loginForm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { Footer } from "@/app/components/footer"; import { Footer } from "@/app/components/footer";
import { createLogger } from "@sourcebot/logger"; import { createLogger } from "@sourcebot/logger";
import { getAuthProviders } from "@/lib/authProviders"; import { getIdentityProviderMetadata } from "@/lib/authProviders";
import { getOrgFromDomain } from "@/data/org"; import { getOrgFromDomain } from "@/data/org";
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
@ -29,7 +29,7 @@ export default async function Signup(props: LoginProps) {
return redirect("/onboard"); return redirect("/onboard");
} }
const providers = getAuthProviders(); const providers = getIdentityProviderMetadata();
return ( return (
<div className="flex flex-col min-h-screen bg-backgroundSecondary"> <div className="flex flex-col min-h-screen bg-backgroundSecondary">
<div className="flex-1 flex flex-col items-center p-4 sm:p-12 w-full"> <div className="flex-1 flex flex-col items-center p-4 sm:p-12 w-full">

View file

@ -13,17 +13,22 @@ import { createTransport } from 'nodemailer';
import { render } from '@react-email/render'; import { render } from '@react-email/render';
import MagicLinkEmail from './emails/magicLinkEmail'; import MagicLinkEmail from './emails/magicLinkEmail';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { getSSOProviders } from '@/ee/features/sso/sso'; import { getEEIdentityProviders } from '@/ee/features/sso/sso';
import { hasEntitlement } from '@sourcebot/shared'; import { hasEntitlement } from '@sourcebot/shared';
import { onCreateUser } from '@/lib/authUtils'; import { onCreateUser } from '@/lib/authUtils';
import { getAuditService } from '@/ee/features/audit/factory'; import { getAuditService } from '@/ee/features/audit/factory';
import { SINGLE_TENANT_ORG_ID } from './lib/constants'; import { SINGLE_TENANT_ORG_ID } from './lib/constants';
const auditService = getAuditService(); const auditService = getAuditService();
const ssoProviders = hasEntitlement("sso") ? await getSSOProviders() : []; const eeIdentityProviders = hasEntitlement("sso") ? await getEEIdentityProviders() : [];
export const runtime = 'nodejs'; export const runtime = 'nodejs';
export type IdentityProvider = {
provider: Provider;
purpose: "sso" | "integration";
}
declare module 'next-auth' { declare module 'next-auth' {
interface Session { interface Session {
user: { user: {
@ -39,10 +44,10 @@ declare module 'next-auth/jwt' {
} }
export const getProviders = () => { 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') { 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, server: env.SMTP_CONNECTION_URL,
from: env.EMAIL_FROM_ADDRESS, from: env.EMAIL_FROM_ADDRESS,
maxAge: 60 * 10, maxAge: 60 * 10,
@ -66,11 +71,11 @@ export const getProviders = () => {
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`); throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
} }
} }
})); }), purpose: "sso"});
} }
if (env.AUTH_CREDENTIALS_LOGIN_ENABLED === 'true') { if (env.AUTH_CREDENTIALS_LOGIN_ENABLED === 'true') {
providers.push(Credentials({ providers.push({ provider: Credentials({
credentials: { credentials: {
email: {}, email: {},
password: {} password: {}
@ -123,7 +128,7 @@ export const getProviders = () => {
}; };
} }
} }
})); }), purpose: "sso"});
} }
return providers; return providers;
@ -193,7 +198,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
return session; return session;
}, },
}, },
providers: getProviders(), providers: getProviders().map((provider) => provider.provider),
pages: { pages: {
signIn: "/login", signIn: "/login",
// We set redirect to false in signInOptions so we can pass the email is as a param // We set redirect to false in signInOptions so we can pass the email is as a param

View file

@ -1,4 +1,3 @@
import type { Provider } from "next-auth/providers";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
import GitHub from "next-auth/providers/github"; import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google"; import Google from "next-auth/providers/google";
@ -14,12 +13,13 @@ import { onCreateUser } from "@/lib/authUtils";
import { createLogger } from "@sourcebot/logger"; import { createLogger } from "@sourcebot/logger";
import { hasEntitlement, loadConfig } from "@sourcebot/shared"; import { hasEntitlement, loadConfig } from "@sourcebot/shared";
import { getTokenFromConfig } from "@sourcebot/crypto"; import { getTokenFromConfig } from "@sourcebot/crypto";
import type { IdentityProvider } from "@/auth";
import { GCPIAPIdentityProviderConfig, GitHubIdentityProviderConfig, GitLabIdentityProviderConfig, GoogleIdentityProviderConfig, KeycloakIdentityProviderConfig, MicrosoftEntraIDIdentityProviderConfig, OktaIdentityProviderConfig } from "@sourcebot/schemas/v3/index.type"; import { GCPIAPIdentityProviderConfig, GitHubIdentityProviderConfig, GitLabIdentityProviderConfig, GoogleIdentityProviderConfig, KeycloakIdentityProviderConfig, MicrosoftEntraIDIdentityProviderConfig, OktaIdentityProviderConfig } from "@sourcebot/schemas/v3/index.type";
const logger = createLogger('web-sso'); const logger = createLogger('web-sso');
export const getSSOProviders = async (): Promise<Provider[]> => { export const getEEIdentityProviders = async (): Promise<IdentityProvider[]> => {
const providers: Provider[] = []; const providers: IdentityProvider[] = [];
const config = env.CONFIG_PATH ? await loadConfig(env.CONFIG_PATH) : undefined; const config = env.CONFIG_PATH ? await loadConfig(env.CONFIG_PATH) : undefined;
const identityProviders = config?.identityProviders ?? []; const identityProviders = config?.identityProviders ?? [];
@ -27,55 +27,49 @@ export const getSSOProviders = async (): Promise<Provider[]> => {
for (const identityProvider of identityProviders) { for (const identityProvider of identityProviders) {
if (identityProvider.provider === "github") { if (identityProvider.provider === "github") {
const providerConfig = identityProvider as GitHubIdentityProviderConfig; const providerConfig = identityProvider as GitHubIdentityProviderConfig;
if (providerConfig.purpose !== "sso") {
continue;
}
const clientId = await getTokenFromConfig(providerConfig.clientId); const clientId = await getTokenFromConfig(providerConfig.clientId);
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
const baseUrl = providerConfig.baseUrl ? await getTokenFromConfig(providerConfig.baseUrl) : undefined; 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") { if (identityProvider.provider === "gitlab") {
const providerConfig = identityProvider as GitLabIdentityProviderConfig; const providerConfig = identityProvider as GitLabIdentityProviderConfig;
if (providerConfig.purpose !== "sso") {
continue;
}
const clientId = await getTokenFromConfig(providerConfig.clientId); const clientId = await getTokenFromConfig(providerConfig.clientId);
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
const baseUrl = providerConfig.baseUrl ? await getTokenFromConfig(providerConfig.baseUrl) : undefined; 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") { if (identityProvider.provider === "google") {
const providerConfig = identityProvider as GoogleIdentityProviderConfig; const providerConfig = identityProvider as GoogleIdentityProviderConfig;
const clientId = await getTokenFromConfig(providerConfig.clientId); const clientId = await getTokenFromConfig(providerConfig.clientId);
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
providers.push(createGoogleProvider(clientId, clientSecret)); providers.push({ provider: createGoogleProvider(clientId, clientSecret), purpose: "sso"});
} }
if (identityProvider.provider === "okta") { if (identityProvider.provider === "okta") {
const providerConfig = identityProvider as OktaIdentityProviderConfig; const providerConfig = identityProvider as OktaIdentityProviderConfig;
const clientId = await getTokenFromConfig(providerConfig.clientId); const clientId = await getTokenFromConfig(providerConfig.clientId);
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
const issuer = await getTokenFromConfig(providerConfig.issuer); 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") { if (identityProvider.provider === "keycloak") {
const providerConfig = identityProvider as KeycloakIdentityProviderConfig; const providerConfig = identityProvider as KeycloakIdentityProviderConfig;
const clientId = await getTokenFromConfig(providerConfig.clientId); const clientId = await getTokenFromConfig(providerConfig.clientId);
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
const issuer = await getTokenFromConfig(providerConfig.issuer); 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") { if (identityProvider.provider === "microsoft-entra-id") {
const providerConfig = identityProvider as MicrosoftEntraIDIdentityProviderConfig; const providerConfig = identityProvider as MicrosoftEntraIDIdentityProviderConfig;
const clientId = await getTokenFromConfig(providerConfig.clientId); const clientId = await getTokenFromConfig(providerConfig.clientId);
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret); const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
const issuer = await getTokenFromConfig(providerConfig.issuer); 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") { if (identityProvider.provider === "gcp-iap") {
const providerConfig = identityProvider as GCPIAPIdentityProviderConfig; const providerConfig = identityProvider as GCPIAPIdentityProviderConfig;
const audience = await getTokenFromConfig(providerConfig.audience); const audience = await getTokenFromConfig(providerConfig.audience);
providers.push(createGCPIAPProvider(audience)); providers.push({ provider: createGCPIAPProvider(audience), purpose: "sso"});
} }
} }

View file

@ -1,18 +1,19 @@
import { getProviders } from "@/auth"; import { getProviders } from "@/auth";
export interface AuthProvider { export interface IdentityProviderMetadata {
id: string; id: string;
name: string; name: string;
purpose: "sso" | "integration";
} }
export const getAuthProviders = (): AuthProvider[] => { export const getIdentityProviderMetadata = (): IdentityProviderMetadata[] => {
const providers = getProviders(); const providers = getProviders();
return providers.map((provider) => { return providers.map((provider) => {
if (typeof provider === "function") { if (typeof provider.provider === "function") {
const providerInfo = provider(); const providerInfo = provider.provider();
return { id: providerInfo.id, name: providerInfo.name }; return { id: providerInfo.id, name: providerInfo.name, purpose: provider.purpose };
} else { } else {
return { id: provider.id, name: provider.name }; return { id: provider.provider.id, name: provider.provider.name, purpose: provider.purpose };
} }
}); });
}; };