From a584a549e81ffaecb4df6b21ad123879c6e17608 Mon Sep 17 00:00:00 2001 From: James Bhattarai Date: Tue, 31 Mar 2026 12:56:52 +0545 Subject: [PATCH] Fix: oidc endpoint --- src/lib/api.ts | 7 + src/pages/org/OIDCClientsPage.tsx | 245 ++++++++++++++++++++++++++---- 2 files changed, 221 insertions(+), 31 deletions(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index 87887ef..50fc380 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1117,6 +1117,13 @@ export const api = { method: 'DELETE', }, true, requestConfig), + // Update OIDC client (name and/or redirect_uris) + updateClient: (orgId: string, clientId: string, data: { name?: string; redirect_uris?: string[] }, requestConfig?: RequestConfig) => + request<{ client: OIDCClient }>(`/organizations/${orgId}/clients/${clientId}`, { + method: 'PATCH', + body: JSON.stringify(data), + }, true, requestConfig), + // Send MFA reminder to a member sendMfaReminder: (orgId: string, userId: string, requestConfig?: RequestConfig) => request<{ message: string }>(`/organizations/${orgId}/members/${userId}/send-mfa-reminder`, { diff --git a/src/pages/org/OIDCClientsPage.tsx b/src/pages/org/OIDCClientsPage.tsx index 71fdae0..8055c86 100644 --- a/src/pages/org/OIDCClientsPage.tsx +++ b/src/pages/org/OIDCClientsPage.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { Plus, Key, MoreHorizontal, Copy, Trash2, Loader2, AlertCircle, CheckCircle, Network, Terminal, Check, - ChevronDown, Globe, RefreshCw, Info, + Globe, RefreshCw, Info, Pencil, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -39,18 +39,45 @@ import { useOrg } from "@/contexts/OrgContext"; const ISSUER_URL = (import.meta.env.VITE_API_BASE_URL ?? "http://localhost:5000/api/v1") .replace(/\/api\/v1\/?$/, ""); -function buildProxyConfig(clientId: string, clientSecret: string, proxyHost: string) { - return `provider = "oidc" -oidc_issuer_url = "${ISSUER_URL}" -client_id = "${clientId}" -client_secret = "${clientSecret}" -redirect_url = "http://${proxyHost}/oauth2/callback" -scope = "openid profile email" -cookie_secret = "$(openssl rand -base64 32 | head -c 32)" -cookie_secure = false -upstream = "http://127.0.0.1:8080/" -set_authorization_header = true -set_x_auth_request_header = true`; +/** Generate a cryptographically random 32-byte base64url cookie secret. */ +function generateCookieSecret(): string { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + // Standard base64, then make it URL-safe (oauth2-proxy accepts both) + return btoa(String.fromCharCode(...bytes)); +} + +function buildProxyConfig( + clientId: string, + clientSecret: string, + proxyHost: string, + upstream: string, + setAuthHeader: boolean, + setXAuthHeader: boolean, + cookieSecret: string, +) { + // Normalise the proxy host — add https:// if no scheme given + const normalizedHost = /^https?:\/\//i.test(proxyHost) + ? proxyHost.replace(/\/$/, "") + : `https://${proxyHost.replace(/\/$/, "")}`; + + // cookie_secure must be true for https, false for plain http + const cookieSecure = normalizedHost.startsWith("https://"); + + const lines = [ + `provider = "oidc"`, + `oidc_issuer_url = "${ISSUER_URL}"`, + `client_id = "${clientId}"`, + `client_secret = "${clientSecret}"`, + `redirect_url = "${normalizedHost}/oauth2/callback"`, + `scope = "openid profile email"`, + `cookie_secret = "${cookieSecret}"`, + `cookie_secure = ${cookieSecure}`, + `upstream = "${upstream || "http://127.0.0.1:8080/"}"`, + ]; + if (setAuthHeader) lines.push(`set_authorization_header = true`); + if (setXAuthHeader) lines.push(`set_x_auth_request_header = true`); + return lines.join("\n"); } function useCopyButton() { @@ -70,6 +97,10 @@ interface NewSecretState { clientId: string; secret: string; proxyHost?: string; + proxyUpstream?: string; + proxySetAuthHeader?: boolean; + proxySetXAuthHeader?: boolean; + proxyCookieSecret?: string; isProxy: boolean; } @@ -92,6 +123,15 @@ export default function OIDCClientsPage() { // Proxy form const proxyNameRef = useRef(null); const proxyHostRef = useRef(null); + const proxyUpstreamRef = useRef(null); + const [proxySetAuthHeader, setProxySetAuthHeader] = useState(true); + const [proxySetXAuthHeader, setProxySetXAuthHeader] = useState(true); + + // Edit state + const [editingClient, setEditingClient] = useState(null); + const [editName, setEditName] = useState(""); + const [editUris, setEditUris] = useState(""); + const [isSavingEdit, setIsSavingEdit] = useState(false); useEffect(() => { if (!orgId) { setIsLoading(false); return; } @@ -117,7 +157,11 @@ export default function OIDCClientsPage() { name = proxyNameRef.current?.value.trim() ?? ""; proxyHost = proxyHostRef.current?.value.trim() ?? ""; if (!name || !proxyHost) return; - uris = [`http://${proxyHost}/oauth2/callback`]; + // Normalise scheme for the registered redirect URI (must match config) + const normalizedHost = /^https?:\/\//i.test(proxyHost) + ? proxyHost.replace(/\/$/, "") + : `https://${proxyHost.replace(/\/$/, "")}`; + uris = [`${normalizedHost}/oauth2/callback`]; } setIsCreating(true); @@ -129,6 +173,10 @@ export default function OIDCClientsPage() { clientId: created.client_id, secret: created.client_secret, proxyHost, + proxyUpstream: proxyUpstreamRef.current?.value.trim() || "http://127.0.0.1:8080/", + proxySetAuthHeader, + proxySetXAuthHeader, + proxyCookieSecret: dialogMode === "proxy" ? generateCookieSecret() : undefined, isProxy: dialogMode === "proxy", }); setDialogMode(null); @@ -150,8 +198,43 @@ export default function OIDCClientsPage() { } }; + const openEditDialog = (client: OIDCClient) => { + setEditingClient(client); + setEditName(client.name); + setEditUris((client.redirect_uris ?? []).join("\n")); + }; + + const handleSaveEdit = async () => { + if (!orgId || !editingClient) return; + const name = editName.trim(); + const uris = editUris.split(/[\n,]+/).map((u) => u.trim()).filter(Boolean); + if (!name || !uris.length) return; + + setIsSavingEdit(true); + try { + const result = await api.organizations.updateClient(orgId, editingClient.id, { name, redirect_uris: uris }); + setClients((prev) => + prev.map((c) => (c.id === editingClient.id ? result.client : c)) + ); + setEditingClient(null); + toast({ title: "Client updated" }); + } catch { + toast({ title: "Error", description: "Failed to update client.", variant: "destructive" }); + } finally { + setIsSavingEdit(false); + } + }; + const proxyConfig = newSecret?.isProxy && newSecret.proxyHost - ? buildProxyConfig(newSecret.clientId, newSecret.secret, newSecret.proxyHost) + ? buildProxyConfig( + newSecret.clientId, + newSecret.secret, + newSecret.proxyHost, + newSecret.proxyUpstream ?? "http://127.0.0.1:8080/", + newSecret.proxySetAuthHeader ?? true, + newSecret.proxySetXAuthHeader ?? true, + newSecret.proxyCookieSecret ?? generateCookieSecret(), + ) : null; return ( @@ -292,6 +375,10 @@ export default function OIDCClientsPage() { + openEditDialog(client)}> + + Edit +
- - + +

- The hostname where oauth2-proxy runs. Redirect URI will be set to{" "} - http://{""}/oauth2/callback automatically. + Full URL where oauth2-proxy is exposed.{" "} + /oauth2/callback will be appended as the redirect URI. +
+ Use https:// in production — cookie_secure is set automatically.

-
- After creating, you'll get a ready-to-paste config snippet for oauth2-proxy. +
+ + +

+ The backend app oauth2-proxy forwards authenticated requests to. +

+
+ +
+ + +
+
+ @@ -389,6 +516,56 @@ export default function OIDCClientsPage() { + {/* Edit dialog */} + { if (!open) setEditingClient(null); }}> + + + Edit OIDC Client + Update the client name and redirect URIs. + +
+
+ + setEditName(e.target.value)} + placeholder="My Application" + /> +
+
+ +