Added OIDC Client CORS attributes

This commit is contained in:
2026-05-19 15:13:51 +00:00
parent 2366847151
commit 91f82fa101
3 changed files with 60 additions and 8 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ services:
build: build:
context: . context: .
args: args:
- VITE_API_BASE_URL=https://secuird.tech/api/v1 - VITE_API_BASE_URL=https://secuird.tech/
container_name: gatehouse-ui container_name: gatehouse-ui
env_file: env_file:
- .env - .env
+5 -4
View File
@@ -1184,10 +1184,10 @@ export const api = {
request<{ clients: OIDCClient[]; count: number }>(`/organizations/${orgId}/clients`, {}, true, requestConfig), request<{ clients: OIDCClient[]; count: number }>(`/organizations/${orgId}/clients`, {}, true, requestConfig),
// Create OIDC client // Create OIDC client
createClient: (orgId: string, name: string, redirect_uris: string[], requestConfig?: RequestConfig) => createClient: (orgId: string, name: string, redirect_uris: string[], allowed_cors_origins?: string[] | null, requestConfig?: RequestConfig) =>
request<{ client: OIDCClientWithSecret }>(`/organizations/${orgId}/clients`, { request<{ client: OIDCClientWithSecret }>(`/organizations/${orgId}/clients`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ name, redirect_uris }), body: JSON.stringify({ name, redirect_uris, allowed_cors_origins }),
}, true, requestConfig), }, true, requestConfig),
// Delete OIDC client // Delete OIDC client
@@ -1196,8 +1196,8 @@ export const api = {
method: 'DELETE', method: 'DELETE',
}, true, requestConfig), }, true, requestConfig),
// Update OIDC client (name and/or redirect_uris) // Update OIDC client (name, redirect_uris, and/or allowed_cors_origins)
updateClient: (orgId: string, clientId: string, data: { name?: string; redirect_uris?: string[] }, requestConfig?: RequestConfig) => updateClient: (orgId: string, clientId: string, data: { name?: string; redirect_uris?: string[]; allowed_cors_origins?: string[] | null }, requestConfig?: RequestConfig) =>
request<{ client: OIDCClient }>(`/organizations/${orgId}/clients/${clientId}`, { request<{ client: OIDCClient }>(`/organizations/${orgId}/clients/${clientId}`, {
method: 'PATCH', method: 'PATCH',
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -1854,6 +1854,7 @@ export interface OIDCClient {
redirect_uris: string[]; redirect_uris: string[];
scopes: string[]; scopes: string[];
grant_types: string[]; grant_types: string[];
allowed_cors_origins: string[] | null;
is_active: boolean; is_active: boolean;
created_at: string; created_at: string;
} }
+54 -3
View File
@@ -119,6 +119,7 @@ export default function OIDCClientsPage() {
// Generic form // Generic form
const nameRef = useRef<HTMLInputElement>(null); const nameRef = useRef<HTMLInputElement>(null);
const urisRef = useRef<HTMLTextAreaElement>(null); const urisRef = useRef<HTMLTextAreaElement>(null);
const corsRef = useRef<HTMLTextAreaElement>(null);
// Proxy form // Proxy form
const proxyNameRef = useRef<HTMLInputElement>(null); const proxyNameRef = useRef<HTMLInputElement>(null);
@@ -131,6 +132,7 @@ export default function OIDCClientsPage() {
const [editingClient, setEditingClient] = useState<OIDCClient | null>(null); const [editingClient, setEditingClient] = useState<OIDCClient | null>(null);
const [editName, setEditName] = useState(""); const [editName, setEditName] = useState("");
const [editUris, setEditUris] = useState(""); const [editUris, setEditUris] = useState("");
const [editCors, setEditCors] = useState("");
const [isSavingEdit, setIsSavingEdit] = useState(false); const [isSavingEdit, setIsSavingEdit] = useState(false);
useEffect(() => { useEffect(() => {
@@ -149,10 +151,16 @@ export default function OIDCClientsPage() {
let uris: string[]; let uris: string[];
let proxyHost: string | undefined; let proxyHost: string | undefined;
let corsOrigins: string[] | null = null;
if (dialogMode === "generic") { if (dialogMode === "generic") {
name = nameRef.current?.value.trim() ?? ""; name = nameRef.current?.value.trim() ?? "";
uris = (urisRef.current?.value ?? "").split(/[\n,]+/).map((u) => u.trim()).filter(Boolean); uris = (urisRef.current?.value ?? "").split(/[\n,]+/).map((u) => u.trim()).filter(Boolean);
if (!name || !uris.length) return; if (!name || !uris.length) return;
const corsRaw = (corsRef.current?.value ?? "").trim();
if (corsRaw) {
corsOrigins = corsRaw.split(/[\n,]+/).map((o) => o.trim()).filter(Boolean);
}
} else { } else {
name = proxyNameRef.current?.value.trim() ?? ""; name = proxyNameRef.current?.value.trim() ?? "";
proxyHost = proxyHostRef.current?.value.trim() ?? ""; proxyHost = proxyHostRef.current?.value.trim() ?? "";
@@ -166,7 +174,7 @@ export default function OIDCClientsPage() {
setIsCreating(true); setIsCreating(true);
try { try {
const result = await api.organizations.createClient(orgId, name, uris); const result = await api.organizations.createClient(orgId, name, uris, corsOrigins);
const created = result.client as OIDCClientWithSecret; const created = result.client as OIDCClientWithSecret;
setClients((prev) => [...prev, created]); setClients((prev) => [...prev, created]);
setNewSecret({ setNewSecret({
@@ -202,6 +210,7 @@ export default function OIDCClientsPage() {
setEditingClient(client); setEditingClient(client);
setEditName(client.name); setEditName(client.name);
setEditUris((client.redirect_uris ?? []).join("\n")); setEditUris((client.redirect_uris ?? []).join("\n"));
setEditCors((client.allowed_cors_origins ?? []).join("\n"));
}; };
const handleSaveEdit = async () => { const handleSaveEdit = async () => {
@@ -210,9 +219,14 @@ export default function OIDCClientsPage() {
const uris = editUris.split(/[\n,]+/).map((u) => u.trim()).filter(Boolean); const uris = editUris.split(/[\n,]+/).map((u) => u.trim()).filter(Boolean);
if (!name || !uris.length) return; if (!name || !uris.length) return;
const corsRaw = editCors.trim();
const corsOrigins: string[] | null = corsRaw
? corsRaw.split(/[\n,]+/).map((o) => o.trim()).filter(Boolean)
: null;
setIsSavingEdit(true); setIsSavingEdit(true);
try { try {
const result = await api.organizations.updateClient(orgId, editingClient.id, { name, redirect_uris: uris }); const result = await api.organizations.updateClient(orgId, editingClient.id, { name, redirect_uris: uris, allowed_cors_origins: corsOrigins });
setClients((prev) => setClients((prev) =>
prev.map((c) => (c.id === editingClient.id ? result.client : c)) prev.map((c) => (c.id === editingClient.id ? result.client : c))
); );
@@ -390,6 +404,16 @@ export default function OIDCClientsPage() {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
{client.allowed_cors_origins && client.allowed_cors_origins.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
<span className="text-xs text-muted-foreground mr-1">CORS:</span>
{client.allowed_cors_origins.map((origin) => (
<Badge key={origin} variant="outline" className="text-xs font-mono">
{origin}
</Badge>
))}
</div>
)}
<div className="mt-3 pt-3 border-t flex items-center justify-between text-xs text-muted-foreground"> <div className="mt-3 pt-3 border-t flex items-center justify-between text-xs text-muted-foreground">
<span>Created {new Date(client.created_at).toLocaleDateString()}</span> <span>Created {new Date(client.created_at).toLocaleDateString()}</span>
<span> <span>
@@ -439,6 +463,19 @@ export default function OIDCClientsPage() {
/> />
<p className="text-xs text-muted-foreground">One URI per line</p> <p className="text-xs text-muted-foreground">One URI per line</p>
</div> </div>
<div className="space-y-2">
<Label htmlFor="corsOrigins">Allowed CORS origins</Label>
<Textarea
id="corsOrigins"
placeholder={"https://myapp.example.com\nhttps://staging.myapp.example.com"}
className="min-h-[60px] font-mono text-sm"
ref={corsRef}
/>
<p className="text-xs text-muted-foreground">
One origin per line (scheme + host + optional port, no path). Leave empty to use the server default.
Use <code className="bg-muted px-1 rounded">+</code> to auto-derive from redirect URIs, or <code className="bg-muted px-1 rounded">*</code> to allow any origin.
</p>
</div>
</TabsContent> </TabsContent>
{/* oauth2-proxy tab */} {/* oauth2-proxy tab */}
@@ -521,7 +558,7 @@ export default function OIDCClientsPage() {
<DialogContent className="sm:max-w-lg"> <DialogContent className="sm:max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle>Edit OIDC Client</DialogTitle> <DialogTitle>Edit OIDC Client</DialogTitle>
<DialogDescription>Update the client name and redirect URIs.</DialogDescription> <DialogDescription>Update the client name, redirect URIs, and CORS origins.</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
<div className="space-y-2"> <div className="space-y-2">
@@ -544,6 +581,20 @@ export default function OIDCClientsPage() {
/> />
<p className="text-xs text-muted-foreground">One URI per line</p> <p className="text-xs text-muted-foreground">One URI per line</p>
</div> </div>
<div className="space-y-2">
<Label htmlFor="editCors">Allowed CORS origins</Label>
<Textarea
id="editCors"
value={editCors}
onChange={(e) => setEditCors(e.target.value)}
placeholder={"https://myapp.example.com\nhttps://staging.myapp.example.com"}
className="min-h-[60px] font-mono text-sm"
/>
<p className="text-xs text-muted-foreground">
One origin per line. Leave empty to use the server default.
Use <code className="bg-muted px-1 rounded">+</code> to auto-derive from redirect URIs, or <code className="bg-muted px-1 rounded">*</code> to allow any origin.
</p>
</div>
{editingClient && ( {editingClient && (
<div className="rounded-md bg-muted/50 border px-3 py-2 space-y-1"> <div className="rounded-md bg-muted/50 border px-3 py-2 space-y-1">
<p className="text-xs text-muted-foreground font-medium">Client ID (read-only)</p> <p className="text-xs text-muted-foreground font-medium">Client ID (read-only)</p>