feat: three-token auth flow (session_id + user_code + device_code)
- session_id (clm_sess_...) in browser URL — identifies login attempt - user_code (ABCD-EFGH) visual confirmation — shown in both terminal and browser - device_code (secret) — CLI polls with this, never displayed - CLI accepts stdin paste of JWT token while polling (race) - Web page handles both ?session= and ?code= params Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export function CliAuthLogin({ code }: Props) {
|
||||
const redirectTo = `/cli-auth?code=${encodeURIComponent(code)}`;
|
||||
const param = code.startsWith("clm_sess_") ? "session" : "code";
|
||||
const redirectTo = `/cli-auth?${param}=${encodeURIComponent(code)}`;
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
@@ -21,7 +21,7 @@ export function DeviceCodeApproval({ code, userName }: Props) {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ user_code: code }),
|
||||
body: JSON.stringify({ session_id: code }),
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.ok) {
|
||||
|
||||
@@ -13,29 +13,30 @@ export const generateMetadata = getMetadata({
|
||||
export default async function CliAuthPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ code?: string; port?: string }>;
|
||||
searchParams: Promise<{ code?: string; session?: string; port?: string }>;
|
||||
}) {
|
||||
const { user } = await getSession();
|
||||
const { code, port } = await searchParams;
|
||||
const { code, session, port } = await searchParams;
|
||||
|
||||
// Device-code flow: code contains "-" (e.g. "ABCD-EFGH"), no port
|
||||
const isDeviceCode = code && code.includes("-") && !port;
|
||||
// New 3-token flow: ?session=clm_sess_... (session_id in URL)
|
||||
// Legacy flow: ?code=ABCD-EFGH (user_code in URL)
|
||||
const sessionId = session ?? (code && code.startsWith("clm_sess_") ? code : null);
|
||||
const isDeviceCode = sessionId || (code && code.includes("-") && !port);
|
||||
const approvalCode = sessionId ?? code;
|
||||
|
||||
if (isDeviceCode) {
|
||||
if (isDeviceCode && approvalCode) {
|
||||
if (!user) {
|
||||
// NOT logged in → show inline auth form with device code context
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--cm-bg,#0a0a0a)] text-[var(--cm-fg,#fafafa)] antialiased flex items-center justify-center">
|
||||
<CliAuthLogin code={code} />
|
||||
<CliAuthLogin code={approvalCode} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// Logged in → auto-approve
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--cm-bg,#0a0a0a)] text-[var(--cm-fg,#fafafa)] antialiased flex items-center justify-center">
|
||||
<DeviceCodeApproval
|
||||
code={code}
|
||||
code={approvalCode}
|
||||
userName={user.name ?? user.email}
|
||||
/>
|
||||
</main>
|
||||
@@ -66,4 +67,3 @@ export default async function CliAuthPage({
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,19 +13,20 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
let body: { user_code?: string };
|
||||
let body: { user_code?: string; session_id?: string };
|
||||
try {
|
||||
body = (await request.json()) as typeof body;
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid body" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!body.user_code) {
|
||||
return NextResponse.json({ error: "user_code required" }, { status: 400 });
|
||||
const code = body.session_id ?? body.user_code;
|
||||
if (!code) {
|
||||
return NextResponse.json({ error: "session_id or user_code required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Proxy approve to the broker
|
||||
const brokerRes = await fetch(`${BROKER_URL}/cli/device-code/${body.user_code}/approve`, {
|
||||
const brokerRes = await fetch(`${BROKER_URL}/cli/device-code/${code}/approve`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user