feat: add manual token authentication for Firebase Studio support

- Add manual token input UI to CloudView component with expandable section
- Add new WebviewMessage type 'rooCloudManualToken' with token field
- Implement handleManualToken method in WebAuthService to process manual tokens
- Add CloudService.handleManualToken method to route manual token auth
- Update webviewMessageHandler to handle manual token submission
- Add translation strings for manual token UI elements

This allows users in Firebase Studio and other IDEs that don't support
automatic authentication redirects to manually paste the token from
the authentication page.

Fixes #7723
This commit is contained in:
Roo Code 2025-09-06 01:27:28 +00:00
parent e8deedd91b
commit 7284fd0b62
6 changed files with 152 additions and 4 deletions

View file

@ -216,6 +216,20 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
return this.authService!.handleCallback(code, state, organizationId)
}
public async handleManualToken(token: string): Promise<void> {
this.ensureInitialized()
if (!this.authService) {
throw new Error("Auth service not available")
}
// For WebAuthService, we need to add a method to handle manual tokens
// Type guard to check if the auth service is WebAuthService
if (this.authService instanceof WebAuthService) {
return this.authService.handleManualToken(token)
} else {
throw new Error("Manual token authentication not supported with current auth service")
}
}
// SettingsService
public getAllowList(): OrganizationAllowList {

View file

@ -330,6 +330,42 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
}
}
/**
* Handle manual token input
*
* This method allows users to manually paste a token from the authentication page
* when automatic redirect doesn't work (e.g., in Firebase Studio)
*
* @param token The authentication token from the URL
*/
public async handleManualToken(token: string): Promise<void> {
if (!token || !token.trim()) {
throw new Error("Invalid token provided")
}
try {
// The token is the ticket that we need to exchange for credentials
const credentials = await this.clerkSignIn(token.trim())
// For manual token, we don't have organization context from the URL
// Set it to null (personal account) by default
credentials.organizationId = null
await this.storeCredentials(credentials)
const vscode = await importVscode()
if (vscode) {
vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud via manual token")
}
this.log("[auth] Successfully authenticated with Roo Code Cloud via manual token")
} catch (error) {
this.log(`[auth] Error handling manual token: ${error}`)
throw new Error(`Failed to authenticate with manual token: ${error}`)
}
}
/**
* Log out
*

View file

@ -2251,6 +2251,35 @@ export const webviewMessageHandler = async (
break
}
case "rooCloudManualToken": {
if (message.token) {
try {
// Extract the actual token from the URL if a full URL is pasted
let token = message.token.trim()
const tokenMatch = token.match(/[?&]token=([^&]+)/)
if (tokenMatch) {
token = tokenMatch[1]
}
// Call the manual token authentication method
await CloudService.instance.handleManualToken(token)
await provider.postStateToWebview()
provider.postMessageToWebview({
type: "authenticatedUser",
userInfo: CloudService.instance.getUserInfo(),
})
} catch (error) {
provider.log(
`Manual token authentication failed: ${error instanceof Error ? error.message : String(error)}`,
)
vscode.window.showErrorMessage(
t("common:errors.manual_token_auth_failed") ||
"Manual token authentication failed. Please check the token and try again.",
)
}
}
break
}
case "saveCodeIndexSettingsAtomic": {
if (!message.codeIndexSettings) {

View file

@ -180,6 +180,7 @@ export interface WebviewMessage {
| "cloudButtonClicked"
| "rooCloudSignIn"
| "rooCloudSignOut"
| "rooCloudManualToken"
| "condenseTaskContextRequest"
| "requestIndexingStatus"
| "startIndexing"
@ -222,6 +223,7 @@ export interface WebviewMessage {
| "removeQueuedMessage"
| "editQueuedMessage"
text?: string
token?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
disabled?: boolean

View file

@ -1,5 +1,5 @@
import { useEffect, useRef } from "react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useRef, useState } from "react"
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { type CloudUserInfo, TelemetryEventName } from "@roo-code/types"
@ -9,7 +9,7 @@ import { vscode } from "@src/utils/vscode"
import { telemetryClient } from "@src/utils/TelemetryClient"
import { ToggleSwitch } from "@/components/ui/toggle-switch"
import { History, PiggyBank, SquareArrowOutUpRightIcon } from "lucide-react"
import { History, PiggyBank, SquareArrowOutUpRightIcon, Key } from "lucide-react"
// Define the production URL constant locally to avoid importing from cloud package in tests
const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com"
@ -25,6 +25,9 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl
const { t } = useAppTranslation()
const { remoteControlEnabled, setRemoteControlEnabled } = useExtensionState()
const wasAuthenticatedRef = useRef(false)
const [showManualToken, setShowManualToken] = useState(false)
const [manualToken, setManualToken] = useState("")
const [tokenError, setTokenError] = useState("")
const rooLogoUri = (window as any).IMAGES_BASE_URI + "/roo-logo.svg"
@ -75,6 +78,26 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl
vscode.postMessage({ type: "remoteControlEnabled", bool: newValue })
}
const handleManualTokenSubmit = () => {
if (!manualToken.trim()) {
setTokenError(t("cloud:manualTokenError") || "Please enter a valid token")
return
}
setTokenError("")
// Extract the token from the URL if a full URL is pasted
let token = manualToken.trim()
const tokenMatch = token.match(/[?&]token=([^&]+)/)
if (tokenMatch) {
token = tokenMatch[1]
}
// Send the manual token to the backend
vscode.postMessage({ type: "rooCloudManualToken", token })
// Clear the token field for security
setManualToken("")
setShowManualToken(false)
}
return (
<div className="flex flex-col h-full">
<div className="flex justify-between items-center mb-6">
@ -192,6 +215,45 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl
<VSCodeButton appearance="primary" onClick={handleConnectClick} className="w-1/2">
{t("cloud:connect")}
</VSCodeButton>
{/* Manual token input section */}
<div className="w-full mt-4">
<button
onClick={() => setShowManualToken(!showManualToken)}
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground underline cursor-pointer bg-transparent border-none p-0 text-sm flex items-center gap-1">
<Key size="14" />
{t("cloud:manualTokenLink") || "Having trouble? Enter token manually"}
</button>
{showManualToken && (
<div className="mt-4 p-4 border border-vscode-widget-border rounded">
<p className="text-sm text-vscode-descriptionForeground mb-3">
{t("cloud:manualTokenDescription") ||
"If you're using Firebase Studio or another IDE that doesn't support automatic authentication, you can paste the token from the authentication page here."}
</p>
<div className="flex gap-2">
<VSCodeTextField
value={manualToken}
onInput={(e: any) => {
setManualToken(e.target.value)
setTokenError("")
}}
placeholder={t("cloud:manualTokenPlaceholder") || "Paste token or URL here"}
className="flex-1"
/>
<VSCodeButton
appearance="secondary"
onClick={handleManualTokenSubmit}
disabled={!manualToken.trim()}>
{t("cloud:submitToken") || "Submit"}
</VSCodeButton>
</div>
{tokenError && (
<p className="text-sm text-vscode-errorForeground mt-2">{tokenError}</p>
)}
</div>
)}
</div>
</div>
</>
)}

View file

@ -13,5 +13,10 @@
"visitCloudWebsite": "Visit Roo Code Cloud",
"remoteControl": "Roomote Control",
"remoteControlDescription": "Enable following and interacting with tasks in this workspace with Roo Code Cloud",
"cloudUrlPillLabel": "Roo Code Cloud URL"
"cloudUrlPillLabel": "Roo Code Cloud URL",
"manualTokenLink": "Having trouble? Enter token manually",
"manualTokenDescription": "If you're using Firebase Studio or another IDE that doesn't support automatic authentication, you can paste the token from the authentication page here.",
"manualTokenPlaceholder": "Paste token or URL here",
"manualTokenError": "Please enter a valid token",
"submitToken": "Submit"
}