Compare commits

...

1 Commits

Author SHA1 Message Date
abeatrix 2195a689b7 Fix: Improve auth flow and state validation
Improves the authentication flow and state validation process.  We no longer reset the auth nounce after each sign in as AuthService is a singleton and there is no risk of nonce collision between different users as only one user can be signed in at a time.

Changes:
    - The `authNonce` is now generated once during `AuthService` instantiation and stored as a read-only property. This ensures that the nonce remains consistent throughout the authentication process.
    - The `resetAuthNonce` method has been removed, as the nonce is no longer meant to be reset.
    - The `createAuthRequest` method now uses the URL object for more graceful query construction.
- **Controller:**
    - The `validateAuthState` method has been simplified to directly compare the provided state with the stored `authNonce`.
- **Extension:**
    - The extension now prompts the user for confirmation if the state parameter in the auth callback does not match the stored `authNonce`. This allows sign-ins initiated from outside the extension (e.g., Cline web) to be handled correctly.

Issue: The issue is that the authNonce is being reset in the validateAuthState method in the Controller, but the extension.ts is directly accessing authService.authNonce without going through the validation method. This creates a race condition where:

User initiates auth, nonce is generated
Auth callback comes back with the state
If there are multiple auth attempts or the callback is processed multiple times, the nonce might be reset before the validation in extension.ts happens
User gets "Invalid auth state" error
2025-07-09 15:19:53 -07:00
3 changed files with 33 additions and 36 deletions
+1 -6
View File
@@ -458,12 +458,7 @@ export class Controller {
// Auth
public async validateAuthState(state: string | null): Promise<boolean> {
const storedNonce = this.authService.authNonce
if (!state || state !== storedNonce) {
return false
}
this.authService.resetAuthNonce() // Clear the nonce after validation
return true
return state === this.authService.authNonce
}
async handleAuthCallback(customToken: string, provider: string | null = null) {
+12 -4
View File
@@ -307,10 +307,18 @@ export async function activate(context: vscode.ExtensionContext) {
provider: provider,
})
// Validate state parameter
if (!(authService.authNonce === state)) {
vscode.window.showErrorMessage("Invalid auth state")
return
// Ask user to confirm on state mismatch. This enables signins initiated from
// outside the extension (e.g. Cline web) to be handled correctly.
if (authService.authNonce !== state) {
const userConfirmation = await vscode.window.showWarningMessage(
`Store token returned from ${uri.path}`,
"Store",
"Cancel",
)
if (userConfirmation === "Cancel") {
console.log("User declined to continue with auth callback due to state mismatch")
return
}
}
if (token) {
+20 -26
View File
@@ -30,7 +30,7 @@ export class AuthService {
private _authenticated: boolean = false
private _user: any = null
private _provider: any = null
private _authNonce: string | null = null
private readonly _authNonce = crypto.randomBytes(32).toString("hex")
private _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler]>()
private _context: vscode.ExtensionContext
@@ -136,7 +136,7 @@ export class AuthService {
this._setProvider(providerName)
}
get authNonce(): string | null {
get authNonce(): string {
return this._authNonce
}
@@ -170,33 +170,27 @@ export class AuthService {
})
}
/**
* Resets the auth nonce to null.
* This is typically called after a successful authentication.
*/
resetAuthNonce(): void {
this._authNonce = null
}
async createAuthRequest(): Promise<String> {
if (!this._authenticated) {
// Generate nonce for state validation
this._authNonce = crypto.randomBytes(32).toString("hex")
const uriScheme = vscode.env.uriScheme
const authUrl = vscode.Uri.parse(
`${this._config.URI}?state=${encodeURIComponent(this._authNonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
)
await vscode.env.openExternal(authUrl)
return String.create({
value: authUrl.toString(),
})
} else {
if (this._authenticated) {
this.sendAuthStatusUpdate()
return String.create({
value: "Already authenticated",
})
return String.create({ value: "Already authenticated" })
}
if (!this._config.URI) {
throw new Error("Authentication URI is not configured")
}
const callbackUrl = `${vscode.env.uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`
// Use URL object for more graceful query construction
const authUrl = new URL(this._config.URI)
authUrl.searchParams.set("state", this._authNonce)
authUrl.searchParams.set("callback_url", callbackUrl)
const authUrlString = authUrl.toString()
await vscode.env.openExternal(vscode.Uri.parse(authUrlString))
return String.create({ value: authUrlString })
}
async handleDeauth(): Promise<void> {