More fixes in SSO

This commit is contained in:
Arunavo Ray
2025-07-26 20:33:26 +05:30
parent 1f6add5fff
commit 0920314679
8 changed files with 1866 additions and 14 deletions

View File

@@ -0,0 +1,10 @@
CREATE TABLE `verifications` (
`id` text PRIMARY KEY NOT NULL,
`identifier` text NOT NULL,
`value` text NOT NULL,
`expires_at` integer NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
);
--> statement-breakpoint
CREATE INDEX `idx_verifications_identifier` ON `verifications` (`identifier`);

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,13 @@
"when": 1752173351102, "when": 1752173351102,
"tag": "0001_polite_exodus", "tag": "0001_polite_exodus",
"breakpoints": true "breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1753539600567,
"tag": "0002_bored_captain_cross",
"breakpoints": true
} }
] ]
} }

View File

@@ -6,11 +6,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { apiRequest, showErrorToast } from '@/lib/utils'; import { apiRequest, showErrorToast } from '@/lib/utils';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Plus, Trash2, ExternalLink, Loader2, AlertCircle, Shield, Info } from 'lucide-react'; import { Plus, Trash2, Loader2, AlertCircle, Shield } from 'lucide-react';
import { Separator } from '@/components/ui/separator';
import { Skeleton } from '../ui/skeleton'; import { Skeleton } from '../ui/skeleton';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -102,7 +100,7 @@ export function SSOSettings() {
setIsLoading(true); setIsLoading(true);
try { try {
const [providersRes, headerAuthStatus] = await Promise.all([ const [providersRes, headerAuthStatus] = await Promise.all([
apiRequest<SSOProvider[]>('/sso/providers'), apiRequest<SSOProvider[] | { providers: SSOProvider[] }>('/sso/providers'),
apiRequest<{ enabled: boolean }>('/auth/header-status').catch(() => ({ enabled: false })) apiRequest<{ enabled: boolean }>('/auth/header-status').catch(() => ({ enabled: false }))
]); ]);
@@ -164,7 +162,7 @@ export function SSOSettings() {
requestData.jwksEndpoint = providerForm.jwksEndpoint; requestData.jwksEndpoint = providerForm.jwksEndpoint;
requestData.userInfoEndpoint = providerForm.userInfoEndpoint; requestData.userInfoEndpoint = providerForm.userInfoEndpoint;
requestData.discoveryEndpoint = providerForm.discoveryEndpoint; requestData.discoveryEndpoint = providerForm.discoveryEndpoint;
requestData.scopes = providerForm.scopes; // Don't send scopes - let the backend handle provider-specific defaults
requestData.pkce = providerForm.pkce; requestData.pkce = providerForm.pkce;
} else { } else {
requestData.entryPoint = providerForm.entryPoint; requestData.entryPoint = providerForm.entryPoint;
@@ -224,10 +222,6 @@ export function SSOSettings() {
}; };
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
toast.success('Copied to clipboard');
};
if (isLoading) { if (isLoading) {
return ( return (
@@ -448,7 +442,14 @@ export function SSOSettings() {
<Alert> <Alert>
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertDescription> <AlertDescription>
Redirect URL: {window.location.origin}/api/auth/sso/callback/{providerForm.providerId || '{provider-id}'} <div className="space-y-2">
<p>Redirect URL: {window.location.origin}/api/auth/sso/callback/{providerForm.providerId || '{provider-id}'}</p>
{providerForm.issuer.includes('google.com') && (
<p className="text-xs text-muted-foreground">
Note: Google doesn't support the "offline_access" scope. The system will automatically use appropriate scopes.
</p>
)}
</div>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</TabsContent> </TabsContent>

View File

@@ -78,6 +78,7 @@ export {
sessions, sessions,
accounts, accounts,
verificationTokens, verificationTokens,
verifications,
oauthApplications, oauthApplications,
oauthAccessTokens, oauthAccessTokens,
oauthConsent, oauthConsent,

View File

@@ -518,6 +518,24 @@ export const verificationTokens = sqliteTable("verification_tokens", {
}; };
}); });
// Verifications table (for Better Auth)
export const verifications = sqliteTable("verifications", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.default(sql`(unixepoch())`),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.default(sql`(unixepoch())`),
}, (table) => {
return {
identifierIdx: index("idx_verifications_identifier").on(table.identifier),
};
});
// ===== OIDC Provider Tables ===== // ===== OIDC Provider Tables =====
// OAuth Applications table // OAuth Applications table

View File

@@ -77,7 +77,7 @@ export async function POST(context: APIContext) {
jwksEndpoint, jwksEndpoint,
discoveryEndpoint, discoveryEndpoint,
userInfoEndpoint, userInfoEndpoint,
scopes = ["openid", "email", "profile"], scopes,
pkce = true, pkce = true,
mapping = { mapping = {
id: "sub", id: "sub",
@@ -88,6 +88,23 @@ export async function POST(context: APIContext) {
} }
} = body; } = 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"];
}
}
registrationBody.oidcConfig = { registrationBody.oidcConfig = {
clientId, clientId,
clientSecret, clientSecret,
@@ -96,7 +113,7 @@ export async function POST(context: APIContext) {
jwksEndpoint, jwksEndpoint,
discoveryEndpoint, discoveryEndpoint,
userInfoEndpoint, userInfoEndpoint,
scopes, scopes: finalScopes,
pkce, pkce,
}; };
registrationBody.mapping = mapping; registrationBody.mapping = mapping;

View File

@@ -13,7 +13,14 @@ export async function GET(context: APIContext) {
const providers = await db.select().from(ssoProviders); const providers = await db.select().from(ssoProviders);
return new Response(JSON.stringify(providers), { // Parse JSON fields before sending
const formattedProviders = providers.map(provider => ({
...provider,
oidcConfig: provider.oidcConfig ? JSON.parse(provider.oidcConfig) : undefined,
samlConfig: provider.samlConfig ? JSON.parse(provider.samlConfig) : undefined,
}));
return new Response(JSON.stringify(formattedProviders), {
status: 200, status: 200,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });
@@ -102,7 +109,14 @@ export async function POST(context: APIContext) {
}) })
.returning(); .returning();
return new Response(JSON.stringify(newProvider), { // Parse JSON fields before sending
const formattedProvider = {
...newProvider,
oidcConfig: newProvider.oidcConfig ? JSON.parse(newProvider.oidcConfig) : undefined,
samlConfig: newProvider.samlConfig ? JSON.parse(newProvider.samlConfig) : undefined,
};
return new Response(JSON.stringify(formattedProvider), {
status: 201, status: 201,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });