Fix: oidc endpoint
This commit is contained in:
@@ -1117,6 +1117,13 @@ export const api = {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}, true, requestConfig),
|
}, 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
|
// Send MFA reminder to a member
|
||||||
sendMfaReminder: (orgId: string, userId: string, requestConfig?: RequestConfig) =>
|
sendMfaReminder: (orgId: string, userId: string, requestConfig?: RequestConfig) =>
|
||||||
request<{ message: string }>(`/organizations/${orgId}/members/${userId}/send-mfa-reminder`, {
|
request<{ message: string }>(`/organizations/${orgId}/members/${userId}/send-mfa-reminder`, {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react";
|
|||||||
import {
|
import {
|
||||||
Plus, Key, MoreHorizontal, Copy, Trash2, Loader2,
|
Plus, Key, MoreHorizontal, Copy, Trash2, Loader2,
|
||||||
AlertCircle, CheckCircle, Network, Terminal, Check,
|
AlertCircle, CheckCircle, Network, Terminal, Check,
|
||||||
ChevronDown, Globe, RefreshCw, Info,
|
Globe, RefreshCw, Info, Pencil,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
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")
|
const ISSUER_URL = (import.meta.env.VITE_API_BASE_URL ?? "http://localhost:5000/api/v1")
|
||||||
.replace(/\/api\/v1\/?$/, "");
|
.replace(/\/api\/v1\/?$/, "");
|
||||||
|
|
||||||
function buildProxyConfig(clientId: string, clientSecret: string, proxyHost: string) {
|
/** Generate a cryptographically random 32-byte base64url cookie secret. */
|
||||||
return `provider = "oidc"
|
function generateCookieSecret(): string {
|
||||||
oidc_issuer_url = "${ISSUER_URL}"
|
const bytes = new Uint8Array(32);
|
||||||
client_id = "${clientId}"
|
crypto.getRandomValues(bytes);
|
||||||
client_secret = "${clientSecret}"
|
// Standard base64, then make it URL-safe (oauth2-proxy accepts both)
|
||||||
redirect_url = "http://${proxyHost}/oauth2/callback"
|
return btoa(String.fromCharCode(...bytes));
|
||||||
scope = "openid profile email"
|
}
|
||||||
cookie_secret = "$(openssl rand -base64 32 | head -c 32)"
|
|
||||||
cookie_secure = false
|
function buildProxyConfig(
|
||||||
upstream = "http://127.0.0.1:8080/"
|
clientId: string,
|
||||||
set_authorization_header = true
|
clientSecret: string,
|
||||||
set_x_auth_request_header = true`;
|
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() {
|
function useCopyButton() {
|
||||||
@@ -70,6 +97,10 @@ interface NewSecretState {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
proxyHost?: string;
|
proxyHost?: string;
|
||||||
|
proxyUpstream?: string;
|
||||||
|
proxySetAuthHeader?: boolean;
|
||||||
|
proxySetXAuthHeader?: boolean;
|
||||||
|
proxyCookieSecret?: string;
|
||||||
isProxy: boolean;
|
isProxy: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +123,15 @@ export default function OIDCClientsPage() {
|
|||||||
// Proxy form
|
// Proxy form
|
||||||
const proxyNameRef = useRef<HTMLInputElement>(null);
|
const proxyNameRef = useRef<HTMLInputElement>(null);
|
||||||
const proxyHostRef = 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(() => {
|
useEffect(() => {
|
||||||
if (!orgId) { setIsLoading(false); return; }
|
if (!orgId) { setIsLoading(false); return; }
|
||||||
@@ -117,7 +157,11 @@ export default function OIDCClientsPage() {
|
|||||||
name = proxyNameRef.current?.value.trim() ?? "";
|
name = proxyNameRef.current?.value.trim() ?? "";
|
||||||
proxyHost = proxyHostRef.current?.value.trim() ?? "";
|
proxyHost = proxyHostRef.current?.value.trim() ?? "";
|
||||||
if (!name || !proxyHost) return;
|
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);
|
setIsCreating(true);
|
||||||
@@ -129,6 +173,10 @@ export default function OIDCClientsPage() {
|
|||||||
clientId: created.client_id,
|
clientId: created.client_id,
|
||||||
secret: created.client_secret,
|
secret: created.client_secret,
|
||||||
proxyHost,
|
proxyHost,
|
||||||
|
proxyUpstream: proxyUpstreamRef.current?.value.trim() || "http://127.0.0.1:8080/",
|
||||||
|
proxySetAuthHeader,
|
||||||
|
proxySetXAuthHeader,
|
||||||
|
proxyCookieSecret: dialogMode === "proxy" ? generateCookieSecret() : undefined,
|
||||||
isProxy: dialogMode === "proxy",
|
isProxy: dialogMode === "proxy",
|
||||||
});
|
});
|
||||||
setDialogMode(null);
|
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
|
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;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -292,6 +375,10 @@ export default function OIDCClientsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => openEditDialog(client)}>
|
||||||
|
<Pencil className="w-4 h-4 mr-2" />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
@@ -361,16 +448,56 @@ export default function OIDCClientsPage() {
|
|||||||
<Input id="proxyName" placeholder="My Protected App" ref={proxyNameRef} />
|
<Input id="proxyName" placeholder="My Protected App" ref={proxyNameRef} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="proxyHost">Proxy host</Label>
|
<Label htmlFor="proxyHost">Proxy public URL</Label>
|
||||||
<Input id="proxyHost" placeholder="app.example.com" ref={proxyHostRef} />
|
<Input id="proxyHost" placeholder="https://app.example.com" ref={proxyHostRef} />
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
The hostname where oauth2-proxy runs. Redirect URI will be set to{" "}
|
Full URL where oauth2-proxy is exposed.{" "}
|
||||||
<code className="bg-muted px-1 rounded">http://{"<host>"}/oauth2/callback</code> automatically.
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md bg-muted/50 border px-3 py-2 text-xs text-muted-foreground">
|
<div className="space-y-2">
|
||||||
After creating, you'll get a ready-to-paste config snippet for oauth2-proxy.
|
<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>
|
||||||
|
<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>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
@@ -389,6 +516,56 @@ export default function OIDCClientsPage() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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 ─────────────────────────────────────────── */}
|
{/* ── Reference ─────────────────────────────────────────── */}
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<div className="flex items-center gap-2 mb-3 text-sm font-medium text-muted-foreground">
|
<div className="flex items-center gap-2 mb-3 text-sm font-medium text-muted-foreground">
|
||||||
@@ -503,7 +680,9 @@ export default function OIDCClientsPage() {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-xs font-medium">1 — Create a client (use the dialog above)</p>
|
<p className="text-xs font-medium">1 — Create a client (use the dialog above)</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -514,9 +693,10 @@ export default function OIDCClientsPage() {
|
|||||||
oidc_issuer_url = "${ISSUER_URL}"
|
oidc_issuer_url = "${ISSUER_URL}"
|
||||||
client_id = "<your-client-id>"
|
client_id = "<your-client-id>"
|
||||||
client_secret = "<your-client-secret>"
|
client_secret = "<your-client-secret>"
|
||||||
redirect_url = "http://<proxy-host>/oauth2/callback"
|
redirect_url = "https://<proxy-host>/oauth2/callback"
|
||||||
scope = "openid profile email"
|
scope = "openid profile email"
|
||||||
cookie_secret = "$(openssl rand -base64 32 | head -c 32)"
|
cookie_secret = "$(openssl rand -base64 32 | head -c 32)"
|
||||||
|
cookie_secure = true
|
||||||
upstream = "http://127.0.0.1:8080/"
|
upstream = "http://127.0.0.1:8080/"
|
||||||
set_authorization_header = true
|
set_authorization_header = true
|
||||||
set_x_auth_request_header = true`}</pre>
|
set_x_auth_request_header = true`}</pre>
|
||||||
@@ -558,8 +738,11 @@ set_x_auth_request_header = true`}</pre>
|
|||||||
OAUTH2_PROXY_CLIENT_ID: \${OIDC_CLIENT_ID}
|
OAUTH2_PROXY_CLIENT_ID: \${OIDC_CLIENT_ID}
|
||||||
OAUTH2_PROXY_CLIENT_SECRET: \${OIDC_CLIENT_SECRET}
|
OAUTH2_PROXY_CLIENT_SECRET: \${OIDC_CLIENT_SECRET}
|
||||||
OAUTH2_PROXY_COOKIE_SECRET: \${COOKIE_SECRET}
|
OAUTH2_PROXY_COOKIE_SECRET: \${COOKIE_SECRET}
|
||||||
|
OAUTH2_PROXY_COOKIE_SECURE: "true"
|
||||||
OAUTH2_PROXY_UPSTREAM: http://app:8080/
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Kubernetes snippet */}
|
{/* Kubernetes snippet */}
|
||||||
|
|||||||
Reference in New Issue
Block a user