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 { 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 (
<>

View file

@ -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 <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 (
<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">

View file

@ -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";
}

View file

@ -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 (
<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">

View file

@ -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();

View file

@ -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 (
<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">

View file

@ -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: {
@ -39,10 +44,10 @@ declare module 'next-auth/jwt' {
}
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

View file

@ -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<Provider[]> => {
const providers: Provider[] = [];
export const getEEIdentityProviders = async (): Promise<IdentityProvider[]> => {
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<Provider[]> => {
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"});
}
}

View file

@ -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 };
}
});
};