Fix: oidc endpoint

This commit is contained in:
2026-03-31 12:56:52 +05:45
parent a0532ba010
commit a584a549e8
2 changed files with 221 additions and 31 deletions
+7
View File
@@ -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`, {
+214 -31
View File
@@ -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<HTMLInputElement>(null);
const proxyHostRef = useRef<HTMLInputElement>(null);
const proxyUpstreamRef = useRef<HTMLInputElement>(null);
const [proxySetAuthHeader, setProxySetAuthHeader] = useState(true);
const [proxySetXAuthHeader, setProxySetXAuthHeader] = useState(true);
// Edit state
const [editingClient, setEditingClient] = useState<OIDCClient | null>(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() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openEditDialog(client)}>
<Pencil className="w-4 h-4 mr-2" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
@@ -361,16 +448,56 @@ export default function OIDCClientsPage() {
<Input id="proxyName" placeholder="My Protected App" ref={proxyNameRef} />
</div>
<div className="space-y-2">
<Label htmlFor="proxyHost">Proxy host</Label>
<Input id="proxyHost" placeholder="app.example.com" ref={proxyHostRef} />
<Label htmlFor="proxyHost">Proxy public URL</Label>
<Input id="proxyHost" placeholder="https://app.example.com" ref={proxyHostRef} />
<p className="text-xs text-muted-foreground">
The hostname where oauth2-proxy runs. Redirect URI will be set to{" "}
<code className="bg-muted px-1 rounded">http://{"<host>"}/oauth2/callback</code> automatically.
Full URL where oauth2-proxy is exposed.{" "}
<code className="bg-muted px-1 rounded">/oauth2/callback</code> will be appended as the redirect URI.
<br />
<span className="text-amber-500/80">Use <code className="bg-muted px-1 rounded">https://</code> in production — <code className="bg-muted px-1 rounded">cookie_secure</code> is set automatically.</span>
</p>
</div>
<div className="rounded-md bg-muted/50 border px-3 py-2 text-xs text-muted-foreground">
After creating, you'll get a ready-to-paste config snippet for oauth2-proxy.
<div className="space-y-2">
<Label htmlFor="proxyUpstream">Upstream (your app)</Label>
<Input
id="proxyUpstream"
placeholder="http://127.0.0.1:8080/"
ref={proxyUpstreamRef}
/>
<p className="text-xs text-muted-foreground">
The backend app oauth2-proxy forwards authenticated requests to.
</p>
</div>
<div className="space-y-2">
<Label className="text-sm">Headers forwarded to upstream</Label>
<div className="space-y-2">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={proxySetAuthHeader}
onChange={(e) => setProxySetAuthHeader(e.target.checked)}
className="w-4 h-4 accent-primary rounded"
/>
<span className="text-sm">
<code className="bg-muted px-1 rounded text-xs">set_authorization_header</code>
<span className="text-muted-foreground ml-1.5 text-xs"> forwards <code className="bg-muted px-1 rounded">Authorization: Bearer </code></span>
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={proxySetXAuthHeader}
onChange={(e) => setProxySetXAuthHeader(e.target.checked)}
className="w-4 h-4 accent-primary rounded"
/>
<span className="text-sm">
<code className="bg-muted px-1 rounded text-xs">set_x_auth_request_header</code>
<span className="text-muted-foreground ml-1.5 text-xs"> forwards <code className="bg-muted px-1 rounded">X-Auth-Request-User</code> / <code className="bg-muted px-1 rounded">X-Auth-Request-Email</code></span>
</span>
</label>
</div>
</div>
</TabsContent>
</Tabs>
@@ -389,6 +516,56 @@ export default function OIDCClientsPage() {
</DialogContent>
</Dialog>
{/* Edit dialog */}
<Dialog open={editingClient !== null} onOpenChange={(open) => { if (!open) setEditingClient(null); }}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Edit OIDC Client</DialogTitle>
<DialogDescription>Update the client name and redirect URIs.</DialogDescription>
</DialogHeader>
<div className="space-y-4 pt-2">
<div className="space-y-2">
<Label htmlFor="editName">Client name</Label>
<Input
id="editName"
value={editName}
onChange={(e) => setEditName(e.target.value)}
placeholder="My Application"
/>
</div>
<div className="space-y-2">
<Label htmlFor="editUris">Redirect URIs</Label>
<Textarea
id="editUris"
value={editUris}
onChange={(e) => setEditUris(e.target.value)}
placeholder={"https://myapp.example.com/callback\nhttps://myapp.example.com/auth/callback"}
className="min-h-[80px] font-mono text-sm"
/>
<p className="text-xs text-muted-foreground">One URI per line</p>
</div>
{editingClient && (
<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>
<code className="text-xs font-mono text-foreground">{editingClient.client_id}</code>
</div>
)}
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="outline" onClick={() => setEditingClient(null)} disabled={isSavingEdit}>
Cancel
</Button>
<Button onClick={handleSaveEdit} disabled={isSavingEdit || !editName.trim()}>
{isSavingEdit ? (
<><Loader2 className="w-4 h-4 mr-2 animate-spin" />Saving</>
) : (
"Save changes"
)}
</Button>
</div>
</DialogContent>
</Dialog>
{/* ── Reference ─────────────────────────────────────────── */}
<div className="mt-8">
<div className="flex items-center gap-2 mb-3 text-sm font-medium text-muted-foreground">
@@ -503,21 +680,24 @@ export default function OIDCClientsPage() {
<div className="space-y-1">
<p className="text-xs font-medium">1 Create a client (use the dialog above)</p>
<p className="text-xs text-muted-foreground">
Set the redirect URI to <code className="bg-muted px-1 rounded">http://&lt;your-proxy-host&gt;/oauth2/callback</code>.
Set the proxy public URL to the address where oauth2-proxy is exposed, e.g.{" "}
<code className="bg-muted px-1 rounded">https://app.example.com</code>. The redirect URI{" "}
<code className="bg-muted px-1 rounded">https://app.example.com/oauth2/callback</code> is registered automatically.
</p>
</div>
{/* Step 2 */}
<div className="space-y-1">
<p className="text-xs font-medium">2 Minimal config</p>
<pre className="text-xs bg-muted rounded p-3 font-mono overflow-x-auto whitespace-pre">{`provider = "oidc"
oidc_issuer_url = "${ISSUER_URL}"
client_id = "<your-client-id>"
client_secret = "<your-client-secret>"
redirect_url = "http://<proxy-host>/oauth2/callback"
scope = "openid profile email"
cookie_secret = "$(openssl rand -base64 32 | head -c 32)"
upstream = "http://127.0.0.1:8080/"
<pre className="text-xs bg-muted rounded p-3 font-mono overflow-x-auto whitespace-pre">{`provider = "oidc"
oidc_issuer_url = "${ISSUER_URL}"
client_id = "<your-client-id>"
client_secret = "<your-client-secret>"
redirect_url = "https://<proxy-host>/oauth2/callback"
scope = "openid profile email"
cookie_secret = "$(openssl rand -base64 32 | head -c 32)"
cookie_secure = true
upstream = "http://127.0.0.1:8080/"
set_authorization_header = true
set_x_auth_request_header = true`}</pre>
</div>
@@ -558,8 +738,11 @@ set_x_auth_request_header = true`}</pre>
OAUTH2_PROXY_CLIENT_ID: \${OIDC_CLIENT_ID}
OAUTH2_PROXY_CLIENT_SECRET: \${OIDC_CLIENT_SECRET}
OAUTH2_PROXY_COOKIE_SECRET: \${COOKIE_SECRET}
OAUTH2_PROXY_COOKIE_SECURE: "true"
OAUTH2_PROXY_UPSTREAM: http://app:8080/
OAUTH2_PROXY_REDIRECT_URL: http://localhost:4180/oauth2/callback`}</pre>
OAUTH2_PROXY_REDIRECT_URL: https://<your-proxy-host>/oauth2/callback
OAUTH2_PROXY_SET_AUTHORIZATION_HEADER: "true"
OAUTH2_PROXY_SET_XAUTHREQUEST: "true"`}</pre>
</div>
{/* Kubernetes snippet */}