diff --git a/src/app/api/oauth/kiro/social-authorize/route.ts b/src/app/api/oauth/kiro/social-authorize/route.ts index 6a557abb43..77003fdede 100755 --- a/src/app/api/oauth/kiro/social-authorize/route.ts +++ b/src/app/api/oauth/kiro/social-authorize/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from "next/server"; -import { generatePKCE } from "@/lib/oauth/utils/pkce"; -import { KiroService } from "@/lib/oauth/services/kiro"; import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; +const KIRO_AUTH_SERVICE = "https://prod.us-east-1.auth.desktop.kiro.dev"; + /** * GET /api/oauth/kiro/social-authorize - * Generate Google/GitHub social login URL for manual callback flow - * Uses kiro:// custom protocol as required by AWS Cognito + * Initiate Google/GitHub social login via device flow. + * Returns a verification URL for the user to open in their browser. */ export async function GET(request) { if ((await isAuthRequired(request)) && !(await isAuthenticated(request))) { @@ -15,7 +15,7 @@ export async function GET(request) { try { const { searchParams } = new URL(request.url); - const provider = searchParams.get("provider"); // "google" or "github" + const provider = searchParams.get("provider"); if (!provider || !["google", "github"].includes(provider)) { return NextResponse.json( @@ -24,17 +24,30 @@ export async function GET(request) { ); } - // Generate PKCE for social auth - const { codeVerifier, codeChallenge, state } = generatePKCE(); + const loginProvider = provider === "google" ? "Google" : "Github"; - const kiroService = new KiroService(); - const authUrl = kiroService.buildSocialLoginUrl(provider, codeChallenge, state); + const response = await fetch(`${KIRO_AUTH_SERVICE}/oauth/device/authorization`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + clientId: "kiro-cli", + loginProvider, + }), + }); + + if (!response.ok) { + const error = await response.text(); + return NextResponse.json({ error: `Device authorization failed: ${error}` }, { status: 502 }); + } + + const data = await response.json(); return NextResponse.json({ - authUrl, - state, - codeVerifier, - codeChallenge, + authUrl: data.verificationUriComplete, + deviceCode: data.deviceCode, + userCode: data.userCode, + expiresIn: Math.floor((data.expiresInMilliseconds || 300000) / 1000), + interval: Math.floor((data.intervalInMilliseconds || 5000) / 1000), provider, }); } catch (error) { diff --git a/src/app/api/oauth/kiro/social-exchange/route.ts b/src/app/api/oauth/kiro/social-exchange/route.ts index 888b600ad9..255f275d71 100755 --- a/src/app/api/oauth/kiro/social-exchange/route.ts +++ b/src/app/api/oauth/kiro/social-exchange/route.ts @@ -3,14 +3,14 @@ import { KiroService } from "@/lib/oauth/services/kiro"; import { createProviderConnection, isCloudEnabled } from "@/models"; import { getConsistentMachineId } from "@/shared/utils/machineId"; import { syncToCloud } from "@/lib/cloudSync"; -import { kiroSocialExchangeSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; +const KIRO_AUTH_SERVICE = "https://prod.us-east-1.auth.desktop.kiro.dev"; + /** * POST /api/oauth/kiro/social-exchange - * Exchange authorization code for tokens (Google/GitHub social login) - * Callback URL will be in format: kiro://kiro.kiroAgent/authenticate-success?code=XXX&state=YYY + * Poll device code for tokens (Google/GitHub social login device flow). + * Frontend calls this repeatedly until authorization completes. */ export async function POST(request: Request) { if ((await isAuthRequired(request)) && !(await isAuthenticated(request))) { @@ -33,61 +33,57 @@ export async function POST(request: Request) { } try { - const validation = validateBody(kiroSocialExchangeSchema, rawBody); - if (isValidationFailure(validation)) { - return NextResponse.json({ error: validation.error }, { status: 400 }); + const { deviceCode, provider } = rawBody; + + if (!deviceCode || !provider) { + return NextResponse.json({ error: "Missing deviceCode or provider" }, { status: 400 }); + } + + const response = await fetch(`${KIRO_AUTH_SERVICE}/oauth/device/poll`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ deviceCode, clientId: "kiro-cli" }), + }); + + const data = await response.json(); + + if (!response.ok || data.error === "authorization_pending" || data.error === "slow_down") { + return NextResponse.json({ + pending: true, + error: data.error || "authorization_pending", + }); + } + + if (!data.accessToken && !data.refreshToken) { + return NextResponse.json({ + pending: true, + error: data.error || "no_tokens", + }); } - const { code, codeVerifier, provider } = validation.data; const kiroService = new KiroService(); + const email = kiroService.extractEmailFromJWT(data.accessToken); - // Exchange code for tokens (redirect_uri handled internally) - const tokenData = await kiroService.exchangeSocialCode(code, codeVerifier); + const providerSpecificData: Record = { + authMethod: "imported", + provider: provider.charAt(0).toUpperCase() + provider.slice(1), + }; - // Register an independent OIDC client for this connection so multiple Kiro accounts - // do not share a single backend session (#2328). Failure is non-fatal; the - // connection will degrade to the shared social-auth refresh path. - let oidcRegistration: { - clientId?: string; - clientSecret?: string; - clientSecretExpiresAt?: number; - } = {}; - try { - oidcRegistration = await kiroService.registerClient(); - } catch (err) { - console.warn("[kiro social-exchange] registerClient failed, continuing without it:", err); + if (data.profileArn) { + providerSpecificData.profileArn = data.profileArn; } - // Extract email from JWT if available - const email = kiroService.extractEmailFromJWT(tokenData.accessToken); - - // Save to database const connection: any = await createProviderConnection({ provider: "kiro", authType: "oauth", - accessToken: tokenData.accessToken, - refreshToken: tokenData.refreshToken, - expiresAt: new Date(Date.now() + tokenData.expiresIn * 1000).toISOString(), + accessToken: data.accessToken, + refreshToken: data.refreshToken, + expiresAt: new Date(Date.now() + (data.expiresIn || 3600) * 1000).toISOString(), email: email || null, - providerSpecificData: { - profileArn: tokenData.profileArn, - authMethod: provider, // "google" or "github" - provider: provider.charAt(0).toUpperCase() + provider.slice(1), - ...(oidcRegistration.clientId - ? { - clientId: oidcRegistration.clientId, - clientSecret: oidcRegistration.clientSecret, - region: "us-east-1", - ...(oidcRegistration.clientSecretExpiresAt - ? { clientSecretExpiresAt: oidcRegistration.clientSecretExpiresAt } - : {}), - } - : {}), - }, + providerSpecificData, testStatus: "active", }); - // Auto sync to Cloud if enabled await syncToCloudIfEnabled(); return NextResponse.json({ @@ -104,14 +100,10 @@ export async function POST(request: Request) { } } -/** - * Sync to Cloud if enabled - */ async function syncToCloudIfEnabled() { try { const cloudEnabled = await isCloudEnabled(); if (!cloudEnabled) return; - const machineId = await getConsistentMachineId(); await syncToCloud(machineId); } catch (error) { diff --git a/src/app/api/settings/import-json/route.ts b/src/app/api/settings/import-json/route.ts index fe1b0688d6..57d07d3405 100644 --- a/src/app/api/settings/import-json/route.ts +++ b/src/app/api/settings/import-json/route.ts @@ -69,7 +69,7 @@ export async function POST(request: Request) { // Re-hydrate the in-memory Global System Prompt config — the migration writes it to // the DB but the in-memory state would stay stale until a restart otherwise (#2470). - const importedSettings = getSettings(); + const importedSettings = await getSettings(); if (importedSettings.systemPrompt) { setSystemPromptConfig(importedSettings.systemPrompt); } diff --git a/src/shared/components/KiroAuthModal.tsx b/src/shared/components/KiroAuthModal.tsx index 05d26a46e7..7d4b3e07ec 100644 --- a/src/shared/components/KiroAuthModal.tsx +++ b/src/shared/components/KiroAuthModal.tsx @@ -174,25 +174,21 @@ export default function KiroAuthModal({

Google Account

-

- Login with your Google account (manual callback). -

+

Login with your Google account.

- {/* GitHub Social Login - HIDDEN */} + {/* GitHub Social Login */} diff --git a/src/shared/components/KiroSocialOAuthModal.tsx b/src/shared/components/KiroSocialOAuthModal.tsx index 64c56d19cc..3af87e72e6 100644 --- a/src/shared/components/KiroSocialOAuthModal.tsx +++ b/src/shared/components/KiroSocialOAuthModal.tsx @@ -1,10 +1,8 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import Modal from "./Modal"; import Button from "./Button"; -import Input from "./Input"; -import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; type KiroSocialOAuthModalProps = { isOpen: boolean; @@ -14,10 +12,6 @@ type KiroSocialOAuthModalProps = { onClose: () => void; }; -/** - * Kiro Social OAuth Modal (Google/GitHub) - * Handles manual callback URL flow for social login - */ export default function KiroSocialOAuthModal({ isOpen, provider, @@ -25,14 +19,12 @@ export default function KiroSocialOAuthModal({ onSuccess, onClose, }: KiroSocialOAuthModalProps) { - const [step, setStep] = useState("loading"); // loading | input | success | error + const [step, setStep] = useState<"loading" | "polling" | "success" | "error">("loading"); + const [error, setError] = useState(null); + const [userCode, setUserCode] = useState(""); const [authUrl, setAuthUrl] = useState(""); - const [authData, setAuthData] = useState(null); - const [callbackUrl, setCallbackUrl] = useState(""); - const [error, setError] = useState(null); - const { copied, copy } = useCopyToClipboard(); + const pollRef = useRef | null>(null); - // Initialize auth flow useEffect(() => { if (!isOpen || !provider) return; @@ -45,69 +37,55 @@ export default function KiroSocialOAuthModal({ const data = await res.json(); if (!res.ok) { - throw new Error(data.error); + throw new Error(data.error || "Failed to start authorization"); } - setAuthData(data); - setAuthUrl(data.authUrl); - setStep("input"); + setUserCode(data.userCode || ""); + setAuthUrl(data.authUrl || ""); + setStep("polling"); - // Auto-open browser - window.open(data.authUrl, "kiro_social_auth"); - } catch (err) { + const interval = (data.interval || 5) * 1000; + pollRef.current = setInterval(async () => { + try { + const pollRes = await fetch("/api/oauth/kiro/social-exchange", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ deviceCode: data.deviceCode, provider }), + }); + const pollData = await pollRes.json(); + + if (pollData.success) { + if (pollRef.current) clearInterval(pollRef.current); + pollRef.current = null; + setStep("success"); + onSuccess?.(); + } + } catch { + // Network error, keep polling + } + }, interval); + } catch (err: any) { setError(err.message); setStep("error"); } }; initAuth(); + + return () => { + if (pollRef.current) { + clearInterval(pollRef.current); + pollRef.current = null; + } + }; }, [isOpen, provider]); - const handleManualSubmit = async () => { - try { - setError(null); - - // Parse callback URL - can be either kiro:// or http://localhost format - let url; - try { - url = new URL(callbackUrl); - } catch (e) { - // If URL parsing fails, might be malformed - throw new Error("Invalid callback URL format"); - } - - const code = url.searchParams.get("code"); - const state = url.searchParams.get("state"); - const errorParam = url.searchParams.get("error"); - - if (errorParam) { - throw new Error(url.searchParams.get("error_description") || errorParam); - } - - if (!code) { - throw new Error("No authorization code found in URL"); - } - - // Exchange code for tokens - const res = await fetch("/api/oauth/kiro/social-exchange", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - code, - codeVerifier: authData.codeVerifier, - provider, - }), - }); - - const data = await res.json(); - if (!res.ok) throw new Error(data.error); - - setStep("success"); - onSuccess?.(); - } catch (err) { - setError(err.message); - setStep("error"); + const handleClose = () => { + if (pollRef.current) { + clearInterval(pollRef.current); + pollRef.current = null; } + onClose(); }; const providerName = provider === "google" ? "Google" : "GitHub"; @@ -116,11 +94,10 @@ export default function KiroSocialOAuthModal({
- {/* Loading */} {step === "loading" && (
@@ -133,50 +110,58 @@ export default function KiroSocialOAuthModal({
)} - {/* Manual Input Step */} - {step === "input" && ( - <> -
-
-

Step 1: Open this URL in your browser

-
- - + {authUrl.length > 80 ? authUrl.slice(0, 80) + "..." : authUrl} + +
- -
-

Step 2: Paste the callback URL here

-

- After authorization, copy the full URL from your browser address bar. -

- setCallbackUrl(e.target.value)} - placeholder="kiro://kiro.kiroAgent/authenticate-success?code=..." - className="font-mono text-xs" - /> + )} + {userCode && ( +
+

Verification code

+

{userCode}

+ )} +
+ + progress_activity + + Waiting for authorization...
- -
- -
- +
)} - {/* Success */} {step === "success" && (
@@ -188,13 +173,12 @@ export default function KiroSocialOAuthModal({

Your {providerLabel} account via {providerName} has been connected.

-
)} - {/* Error */} {step === "error" && (
@@ -203,11 +187,8 @@ export default function KiroSocialOAuthModal({

Connection Failed

{error}

- -