Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions frontend/src/app/(auth)/login/ApiKeyLoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Button } from "@/shared/components/ui/button";
import { Input } from "@/shared/components/ui/input";
import { LoaderCircle, ArrowRight, Key } from "lucide-react";
import { useAuthContext } from "../AuthLayoutClient";
import { setApiKeyCookieAction } from "@/app/actions";

const containerVariants = {
hidden: { opacity: 0 },
Expand Down Expand Up @@ -67,8 +68,9 @@ export function ApiKeyLoginForm() {
const data = await response.json();

if (data.valid) {
// Store API key in cookie via the existing connection mechanism
document.cookie = `lg_apiKey=${encodeURIComponent(apiKey.trim())}; path=/; max-age=${365 * 24 * 3600}; samesite=lax`;
// Store the API key as an httpOnly cookie via a server action so it is
// not readable from JS (mitigates XSS exfiltration).
await setApiKeyCookieAction(apiKey.trim());
router.push("/");
router.refresh();
} else {
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,35 @@ export async function updateConnectionAction(connection: {
return { success: true };
}

/**
* Server action to set only the API key as an httpOnly cookie.
*
* Used by the API-key login form, which collects only an apiKey (no apiUrl).
* Replaces direct `document.cookie =` writes that left the key JS-readable.
*/
export async function setApiKeyCookieAction(apiKey: string) {
await requireAuth();
const trimmed = apiKey.trim();

const cookieStore = await cookies();

if (!trimmed) {
cookieStore.delete(CONNECTION_COOKIE_NAMES.apiKey);
return { success: true };
}

const isProduction = process.env.NODE_ENV === "production";
cookieStore.set(CONNECTION_COOKIE_NAMES.apiKey, trimmed, {
path: "/",
maxAge: COOKIE_MAX_AGE,
sameSite: "lax" as const,
httpOnly: isProduction,
secure: isProduction,
});

return { success: true };
}

/**
* Server action to update only the assistantId
*/
Expand Down
10 changes: 0 additions & 10 deletions frontend/src/lib/api-key.tsx

This file was deleted.

5 changes: 3 additions & 2 deletions frontend/src/providers/Thread.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { validate } from "uuid";
import { Thread, Client } from "@langchain/langgraph-sdk";
import {
Expand All @@ -10,7 +12,6 @@ import {
SetStateAction,
} from "react";
import { createClient } from "./client";
import { getApiKey } from "@/lib/api-key";
import type { ConnectionConfig } from "./Stream";

export interface ThreadContextType {
Expand Down Expand Up @@ -45,7 +46,7 @@ export function ThreadProvider({ children, connection }: ThreadProviderProps) {
const [threads, setThreads] = useState<Thread[]>([]);
const [threadsLoading, setThreadsLoading] = useState(false);
const finalAssistantId = connection.assistantId?.trim() || undefined;
const apiKey = connection.apiKey || getApiKey() || undefined;
const apiKey = connection.apiKey || undefined;

// Create client once and memoize
const client = useMemo(() => {
Expand Down
8 changes: 2 additions & 6 deletions frontend/src/shared/components/settings/ConnectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,10 @@ export function ConnectionList({ onConnectionChange }: ConnectionListProps) {

await switchConnection(connection.id);

// Save API key to localStorage if provided
if (connection.apiKey) {
localStorage.setItem("lg:chat:apiKey", connection.apiKey);
}

toast.success(`Switching to ${connection.name}...`);

// Reload page to apply connection (cookies are already set by switchConnection)
// Reload page to apply connection (cookies are already set by switchConnection
// via updateConnectionAction, which uses httpOnly cookies in production).
setTimeout(() => {
window.location.href = window.location.pathname;
}, 500);
Expand Down
Loading