Fix: oidc endpoint
This commit is contained in:
@@ -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`, {
|
||||
|
||||
@@ -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://<your-proxy-host>/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 */}
|
||||
|
||||
Reference in New Issue
Block a user