fix(sso): harden SSO with role sync, security fixes, design compliance, and test coverage (#564)

Rate-limit OIDC callback route and clear state cookie on all paths.
Validate GitHub API responses and add server-side config validation.
Sync SSO user roles on every login respecting seat limits.
Add LDAP connection timeout, bind DN warning, and multiple entry logging.
Replace Select with Combobox and apply card design tokens in SSOSection.
Expose OIDC scopes field in settings UI.
Fix hardcoded colors to use design system tokens.
Add standard and diagnostic logging throughout SSO flow.
Add tests for role sync, seat limits, LDAP escaping, and config validation.
Remove unused login-form.tsx template.
Update docs and screenshots for SSO feature.
This commit is contained in:
Anso
2026-04-13 17:46:38 -04:00
committed by GitHub
parent d184b7134b
commit 1d89e8ce59
11 changed files with 308 additions and 107 deletions
+1 -1
View File
@@ -159,7 +159,7 @@ export function Login({
/>
</div>
{error && (
<div className="text-sm text-red-500 text-center">
<div className="text-sm text-destructive text-center">
{error}
</div>
)}
+34 -24
View File
@@ -3,7 +3,7 @@ import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Combobox } from '@/components/ui/combobox';
import { Badge } from '@/components/ui/badge';
import { toast } from '@/components/ui/toast-store';
import { apiFetch } from '@/lib/api';
@@ -12,6 +12,11 @@ import { CapabilityGate } from './CapabilityGate';
import { TierBadge } from './TierBadge';
import { Shield, Loader2, CheckCircle, XCircle } from 'lucide-react';
const ROLE_OPTIONS = [
{ value: 'viewer', label: 'Viewer' },
{ value: 'admin', label: 'Admin' },
];
interface SSOProviderConfig {
provider: string;
enabled: boolean;
@@ -120,7 +125,7 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
};
return (
<div className="border border-border rounded-lg">
<div className="rounded-lg border border-card-border border-t-card-border-top bg-card text-card-foreground shadow-card-bevel transition-colors hover:border-t-card-border-hover">
<div
className="flex items-center justify-between p-4 cursor-pointer hover:bg-muted/30 transition-colors"
onClick={() => setExpanded(!expanded)}
@@ -204,16 +209,12 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
</div>
<div className="grid gap-2">
<Label className="text-xs text-muted-foreground">Default Role</Label>
<Select
<Combobox
options={ROLE_OPTIONS}
value={config.ldapDefaultRole || 'viewer'}
onValueChange={v => update('ldapDefaultRole', v)}
>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
placeholder="Select role"
/>
</div>
</div>
<div className="flex items-center gap-2">
@@ -273,18 +274,27 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
/>
</div>
</div>
<div className="grid gap-2">
<Label className="text-xs text-muted-foreground">Default Role</Label>
<Select
value={config.oidcDefaultRole || 'viewer'}
onValueChange={v => update('oidcDefaultRole', v)}
>
<SelectTrigger className="w-[140px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-2">
<Label className="text-xs text-muted-foreground">Scopes</Label>
<Input
placeholder="openid email profile"
value={config.oidcScopes || ''}
onChange={e => update('oidcScopes', e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Space-separated list of OAuth scopes. Leave blank for default.
</p>
</div>
<div className="grid gap-2">
<Label className="text-xs text-muted-foreground">Default Role</Label>
<Combobox
options={ROLE_OPTIONS}
value={config.oidcDefaultRole || 'viewer'}
onValueChange={v => update('oidcDefaultRole', v)}
placeholder="Select role"
/>
</div>
</div>
</>
)}
@@ -300,11 +310,11 @@ function ProviderCard({ providerId, type, label, initialConfig, onSave }: {
{testResult && (
testResult.success
? <CheckCircle className="w-4 h-4 text-success" />
: <XCircle className="w-4 h-4 text-red-500" />
: <XCircle className="w-4 h-4 text-destructive" />
)}
</div>
{initialConfig && (
<Button size="sm" variant="ghost" className="text-red-500 hover:text-red-400" onClick={handleDelete}>
<Button size="sm" variant="ghost" className="text-destructive/60 hover:bg-destructive hover:text-destructive-foreground" onClick={handleDelete}>
Remove
</Button>
)}
-68
View File
@@ -1,68 +0,0 @@
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function LoginForm({
className,
...props
}: React.ComponentPropsWithoutRef<"div">) {
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<a
href="#"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<Input id="password" type="password" required />
</div>
<Button type="submit" className="w-full">
Login
</Button>
<Button variant="outline" className="w-full">
Login with Google
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
<a href="#" className="underline underline-offset-4">
Sign up
</a>
</div>
</form>
</CardContent>
</Card>
</div>
)
}