-
-
-
{provider.providerId}
-
- {provider.samlConfig ? 'SAML' : 'OIDC'}
-
-
-
{provider.domain}
+
+
+
+
+
{provider.providerId}
+
+ {provider.samlConfig ? 'SAML' : 'OIDC'}
+
+
{provider.domain}
+
+
+
+ Issuer:
+ {provider.issuer}
+
+
+ {provider.oidcConfig && (
+ <>
+
+ Client ID:
+ {provider.oidcConfig.clientId}
+
+
+ {provider.oidcConfig.scopes && provider.oidcConfig.scopes.length > 0 && (
+
+
Scopes:
+
+ {provider.oidcConfig.scopes.map(scope => (
+
+ {scope}
+
+ ))}
+
+
+ )}
+ >
+ )}
+
+ {provider.samlConfig && (
+
+ Entry Point:
+ {provider.samlConfig.entryPoint}
+
+ )}
+
+ {provider.organizationId && (
+
+ Organization:
+ {provider.organizationId}
+
+ )}
+
+
+
+
+
-
-
-
-
-
Issuer
-
{provider.issuer}
-
- {provider.oidcConfig && (
-
-
Client ID
-
{provider.oidcConfig.clientId}
-
- )}
- {provider.samlConfig && (
-
-
Entry Point
-
{provider.samlConfig.entryPoint}
-
- )}
- {provider.organizationId && (
-
-
Organization
-
{provider.organizationId}
-
- )}
-
-
-
+
+
))}
)}
diff --git a/src/components/ui/multi-select.tsx b/src/components/ui/multi-select.tsx
new file mode 100644
index 0000000..3cbe3b4
--- /dev/null
+++ b/src/components/ui/multi-select.tsx
@@ -0,0 +1,137 @@
+import * as React from "react"
+import { X } from "lucide-react"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
+
+interface MultiSelectProps {
+ options: { label: string; value: string }[]
+ selected: string[]
+ onChange: (selected: string[]) => void
+ placeholder?: string
+ className?: string
+}
+
+export function MultiSelect({
+ options,
+ selected,
+ onChange,
+ placeholder = "Select items...",
+ className,
+}: MultiSelectProps) {
+ const [open, setOpen] = React.useState(false)
+
+ const handleUnselect = (item: string) => {
+ onChange(selected.filter((i) => i !== item))
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ No item found.
+
+ {options.map((option) => (
+ {
+ onChange(
+ selected.includes(option.value)
+ ? selected.filter((item) => item !== option.value)
+ : [...selected, option.value]
+ )
+ setOpen(true)
+ }}
+ >
+
+ {option.label}
+
+ ))}
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/hooks/useAuthMethods.ts b/src/hooks/useAuthMethods.ts
index 9f77a69..273fc1b 100644
--- a/src/hooks/useAuthMethods.ts
+++ b/src/hooks/useAuthMethods.ts
@@ -35,8 +35,8 @@ export function useAuthMethods() {
const loadAuthMethods = async () => {
try {
- // Check SSO providers
- const providers = await apiRequest
('/sso/providers').catch(() => []);
+ // Check SSO providers - use public endpoint since this is used on login page
+ const providers = await apiRequest('/sso/providers/public').catch(() => []);
const applications = await apiRequest('/sso/applications').catch(() => []);
setAuthMethods({
diff --git a/src/pages/api/auth/sso/register.ts b/src/pages/api/auth/sso/register.ts
index 8635188..ef10489 100644
--- a/src/pages/api/auth/sso/register.ts
+++ b/src/pages/api/auth/sso/register.ts
@@ -88,22 +88,8 @@ export async function POST(context: APIContext) {
}
} = body;
- // Handle provider-specific scope defaults
- let finalScopes = scopes;
- if (!finalScopes) {
- // Check if this is a Google provider
- const isGoogle = issuer.includes('google.com') ||
- issuer.includes('googleapis.com') ||
- domain.includes('google.com');
-
- if (isGoogle) {
- // Google doesn't support offline_access scope
- finalScopes = ["openid", "email", "profile"];
- } else {
- // Default scopes for other providers
- finalScopes = ["openid", "email", "profile", "offline_access"];
- }
- }
+ // Use provided scopes or default if not specified
+ const finalScopes = scopes || ["openid", "email", "profile"];
registrationBody.oidcConfig = {
clientId,
diff --git a/src/pages/api/sso/providers.ts b/src/pages/api/sso/providers.ts
index 66564e0..5b4eb60 100644
--- a/src/pages/api/sso/providers.ts
+++ b/src/pages/api/sso/providers.ts
@@ -17,7 +17,7 @@ export async function GET(context: APIContext) {
const formattedProviders = providers.map(provider => ({
...provider,
oidcConfig: provider.oidcConfig ? JSON.parse(provider.oidcConfig) : undefined,
- samlConfig: provider.samlConfig ? JSON.parse(provider.samlConfig) : undefined,
+ samlConfig: (provider as any).samlConfig ? JSON.parse((provider as any).samlConfig) : undefined,
}));
return new Response(JSON.stringify(formattedProviders), {
@@ -48,6 +48,7 @@ export async function POST(context: APIContext) {
mapping,
providerId,
organizationId,
+ scopes,
} = body;
// Validate required fields
@@ -86,6 +87,7 @@ export async function POST(context: APIContext) {
tokenEndpoint,
jwksEndpoint,
userInfoEndpoint,
+ scopes: scopes || ["openid", "email", "profile"],
mapping: mapping || {
id: "sub",
email: "email",
@@ -113,7 +115,7 @@ export async function POST(context: APIContext) {
const formattedProvider = {
...newProvider,
oidcConfig: newProvider.oidcConfig ? JSON.parse(newProvider.oidcConfig) : undefined,
- samlConfig: newProvider.samlConfig ? JSON.parse(newProvider.samlConfig) : undefined,
+ samlConfig: (newProvider as any).samlConfig ? JSON.parse((newProvider as any).samlConfig) : undefined,
};
return new Response(JSON.stringify(formattedProvider), {
@@ -125,6 +127,100 @@ export async function POST(context: APIContext) {
}
}
+// PUT /api/sso/providers - Update an existing SSO provider
+export async function PUT(context: APIContext) {
+ try {
+ const { user, response } = await requireAuth(context);
+ if (response) return response;
+
+ const url = new URL(context.request.url);
+ const providerId = url.searchParams.get("id");
+
+ if (!providerId) {
+ return new Response(
+ JSON.stringify({ error: "Provider ID is required" }),
+ {
+ status: 400,
+ headers: { "Content-Type": "application/json" },
+ }
+ );
+ }
+
+ const body = await context.request.json();
+ const {
+ issuer,
+ domain,
+ clientId,
+ clientSecret,
+ authorizationEndpoint,
+ tokenEndpoint,
+ jwksEndpoint,
+ userInfoEndpoint,
+ scopes,
+ organizationId,
+ } = body;
+
+ // Get existing provider
+ const [existingProvider] = await db
+ .select()
+ .from(ssoProviders)
+ .where(eq(ssoProviders.id, providerId))
+ .limit(1);
+
+ if (!existingProvider) {
+ return new Response(
+ JSON.stringify({ error: "Provider not found" }),
+ {
+ status: 404,
+ headers: { "Content-Type": "application/json" },
+ }
+ );
+ }
+
+ // Parse existing config
+ const existingConfig = JSON.parse(existingProvider.oidcConfig);
+
+ // Create updated OIDC config
+ const updatedOidcConfig = {
+ ...existingConfig,
+ clientId: clientId || existingConfig.clientId,
+ clientSecret: clientSecret || existingConfig.clientSecret,
+ authorizationEndpoint: authorizationEndpoint || existingConfig.authorizationEndpoint,
+ tokenEndpoint: tokenEndpoint || existingConfig.tokenEndpoint,
+ jwksEndpoint: jwksEndpoint || existingConfig.jwksEndpoint,
+ userInfoEndpoint: userInfoEndpoint || existingConfig.userInfoEndpoint,
+ scopes: scopes || existingConfig.scopes || ["openid", "email", "profile"],
+ };
+
+ // Update provider
+ const [updatedProvider] = await db
+ .update(ssoProviders)
+ .set({
+ issuer: issuer || existingProvider.issuer,
+ domain: domain || existingProvider.domain,
+ oidcConfig: JSON.stringify(updatedOidcConfig),
+ organizationId: organizationId !== undefined ? organizationId : existingProvider.organizationId,
+ updatedAt: new Date(),
+ })
+ .where(eq(ssoProviders.id, providerId))
+ .returning();
+
+ // Parse JSON fields before sending
+ const formattedProvider = {
+ ...updatedProvider,
+ oidcConfig: JSON.parse(updatedProvider.oidcConfig),
+ samlConfig: (updatedProvider as any).samlConfig ? JSON.parse((updatedProvider as any).samlConfig) : undefined,
+ };
+
+ return new Response(JSON.stringify(formattedProvider), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+ } catch (error) {
+ return createSecureErrorResponse(error, "SSO providers API");
+ }
+}
+
// DELETE /api/sso/providers - Delete a provider by ID
export async function DELETE(context: APIContext) {
try {
diff --git a/src/pages/api/sso/providers/public.ts b/src/pages/api/sso/providers/public.ts
new file mode 100644
index 0000000..2442766
--- /dev/null
+++ b/src/pages/api/sso/providers/public.ts
@@ -0,0 +1,22 @@
+import type { APIContext } from "astro";
+import { createSecureErrorResponse } from "@/lib/utils";
+import { db, ssoProviders } from "@/lib/db";
+
+// GET /api/sso/providers/public - Get public SSO provider information for login page
+export async function GET(context: APIContext) {
+ try {
+ // Get all providers but only return public information
+ const providers = await db.select({
+ id: ssoProviders.id,
+ providerId: ssoProviders.providerId,
+ domain: ssoProviders.domain,
+ }).from(ssoProviders);
+
+ return new Response(JSON.stringify(providers), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+ } catch (error) {
+ return createSecureErrorResponse(error, "Public SSO providers API");
+ }
+}
\ No newline at end of file