-
Notifications
You must be signed in to change notification settings - Fork 8
Feat: Add URL-encoding detection and retry logic for cluster authentication #454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: rel/release-0.7.1
Are you sure you want to change the base?
Feat: Add URL-encoding detection and retry logic for cluster authentication #454
Conversation
…x it for the user
|
@microsoft-github-policy-service agree |
|
Hey @vaibh123540! Thank you so much for your contribution and for taking on this issue. Please excuse the delay in getting back to you. You did a lot of things very well here:
I'd suggest improvements here:
It's not easy to catch all "invalid password"-style error messages/codes across platforms (DocumentDB, Atlas, etc.), but we can have a "black list" for errors we are certain are network related. Here a generated example of what it could be: /**
* Network error codes where URL-encoding retry is not relevant.
* These are OS-level error codes that indicate connectivity issues,
* not authentication problems.
*/
const NETWORK_ERROR_PATTERNS = [
'ECONNREFUSED', // Connection refused (server not running)
'ENOTFOUND', // DNS lookup failed
'ETIMEDOUT', // Connection timed out
'ENETUNREACH', // Network unreachable
'EHOSTUNREACH', // Host unreachable
'EAI_AGAIN', // DNS temporary failure
'ECONNRESET', // Connection reset by peer
'EPIPE', // Broken pipe
'CERT_', // Certificate errors (self-signed, expired, etc.)
'self-signed certificate', // TLS/SSL cert issue
] as const;
function isNetworkError(errorMessage: string): boolean {
const upperMessage = errorMessage.toUpperCase();
return NETWORK_ERROR_PATTERNS.some((pattern) => upperMessage.includes(pattern.toUpperCase()));
}
Please let me know whether you've got the time to continue contributing to this PR or whether we should reassign it. |
|
@vaibh123540 FYI: I merged current |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds URL-encoding detection and retry logic for cluster authentication to improve user experience when users accidentally provide URL-encoded passwords. When a connection fails, the code detects if the password contains URL-encoded characters and offers a one-time retry with the decoded password.
Changes:
- Added URL-encoding detection using regex pattern
/%[0-9A-Fa-f]{2}/anddecodeURIComponent - Added conditional error dialog with "Retry with Decoded Password" button when URL-encoding is detected
- Implemented retry logic that updates credentials in both cache and storage with the decoded password on success
- Enhanced error messages to inform users about potential URL-encoding issues
| const result = await vscode.window.showErrorMessage( | ||
| l10n.t('Failed to connect to "{cluster}"', { cluster: this.cluster.name }), | ||
| { | ||
| modal: true, | ||
| detail: | ||
| l10n.t('Revisit connection details and try again.') + | ||
| '\n\n' + | ||
| l10n.t('Error: {error}', { error: (error as Error).message }), | ||
| }, | ||
| { modal: true, detail: detailMessage }, | ||
| ...buttons, | ||
| ); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable name 'result' shadows the outer variable 'result' from the callWithTelemetryAndErrorHandling callback function scope. This can lead to confusion and potential bugs. Consider renaming this variable to something more specific like 'userResponse' or 'dialogResult' to avoid shadowing.
| let detailMessage: string = | ||
| l10n.t('Revisit connection details and try again.') + | ||
| '\n\n' + | ||
| l10n.t('Error: {error}', { error: (error as Error).message }); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling uses (error as Error).message without verifying the error is actually an Error instance. While this works in most cases, it could fail if the caught error is not an Error object. For consistency with the retry error handling (line 289 which uses retryErr instanceof Error), consider using the same defensive pattern to ensure robust error message extraction.
| l10n.t('Revisit connection details and try again.') + | ||
| '\n\n' + | ||
| l10n.t('Error: {error}', { error: (error as Error).message }); | ||
|
|
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex pattern /%[0-9A-Fa-f]{2}/ only detects passwords that contain URL-encoded characters, but it doesn't verify that the entire password is URL-encoded or that the encoding is valid. This could result in false positives for passwords that legitimately contain sequences like "%20" as part of their actual password text (not as encoding). Consider adding additional validation to reduce false positives, or document this limitation in a comment explaining that some legitimate passwords may trigger this detection.
| // Heuristic: detect presence of URL-encoded-looking sequences in the password. | |
| // This does not guarantee that the entire password is URL-encoded or that the | |
| // encoding is valid; some legitimate passwords may contain sequences like "%20" | |
| // and will still trigger this hint. We rely on a successful decode and a changed | |
| // value (password !== decodedPassword) before offering the retry. |
| // URL-Encoding checks | ||
| let decodedPassword: string | undefined; | ||
| try { | ||
| decodedPassword = password ? decodeURIComponent(password) : undefined; | ||
| } catch { | ||
| decodedPassword = undefined; | ||
| } | ||
|
|
||
| const retryButton = l10n.t('Retry with Decoded Password'); | ||
| const buttons: string[] = []; | ||
|
|
||
| let detailMessage: string = | ||
| l10n.t('Revisit connection details and try again.') + | ||
| '\n\n' + | ||
| l10n.t('Error: {error}', { error: (error as Error).message }); | ||
|
|
||
| const shouldOfferRetry = | ||
| password && /%[0-9A-Fa-f]{2}/.test(password) && decodedPassword && password !== decodedPassword; | ||
|
|
||
| if (shouldOfferRetry) { | ||
| detailMessage += '\n\n' + l10n.t('Your password appears to be URL-encoded.'); | ||
| buttons.push(retryButton); | ||
| } | ||
|
|
||
| // Offer a one-time retry using the decoded value | ||
| const result = await vscode.window.showErrorMessage( | ||
| l10n.t('Failed to connect to "{cluster}"', { cluster: this.cluster.name }), | ||
| { | ||
| modal: true, | ||
| detail: | ||
| l10n.t('Revisit connection details and try again.') + | ||
| '\n\n' + | ||
| l10n.t('Error: {error}', { error: (error as Error).message }), | ||
| }, | ||
| { modal: true, detail: detailMessage }, | ||
| ...buttons, | ||
| ); | ||
|
|
||
| if (result === retryButton && decodedPassword && username && authMethod) { | ||
| context.valuesToMask.push(decodedPassword); | ||
|
|
||
| CredentialCache.setAuthCredentials( | ||
| this.id, | ||
| authMethod, | ||
| connectionString.toString(), | ||
| { connectionUser: username, connectionPassword: decodedPassword }, | ||
| this.cluster.emulatorConfiguration, | ||
| connectionCredentials.secrets.entraIdAuthConfig, | ||
| ); | ||
|
|
||
| await ClustersClient.deleteClient(this.id); | ||
|
|
||
| try { | ||
| clustersClient = await ClustersClient.getClient(this.id); | ||
|
|
||
| connectionCredentials.secrets.nativeAuthConfig = { | ||
| connectionUser: username, | ||
| connectionPassword: decodedPassword, | ||
| }; | ||
| await ConnectionStorageService.save(connectionType, connectionCredentials, true); | ||
|
|
||
| ext.outputChannel.appendLine( | ||
| l10n.t('Connected to the cluster "{cluster}" using decoded password.', { | ||
| cluster: this.cluster.name, | ||
| }), | ||
| ); | ||
| return clustersClient; | ||
| } catch (retryErr: unknown) { | ||
| const retryError = retryErr instanceof Error ? retryErr : new Error(String(retryErr)); | ||
| ext.outputChannel.appendLine(l10n.t('Retry Error: {error}', { error: retryError.message })); | ||
|
|
||
| // Show final error screen without the URL hint | ||
| void vscode.window.showErrorMessage( | ||
| l10n.t('Failed to connect to "{cluster}"', { cluster: this.cluster.name }), | ||
| { | ||
| modal: true, | ||
| detail: | ||
| l10n.t('Revisit connection details and try again.') + | ||
| '\n\n' + | ||
| l10n.t('Error: {error}', { error: retryError.message }), | ||
| }, | ||
| ); | ||
| } | ||
| } |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The URL-encoding detection and retry logic lacks test coverage. This new feature introduces complex conditional logic, user interaction flows, and credential management that should be validated with automated tests. Consider adding tests for: (1) detection of URL-encoded passwords, (2) successful retry with decoded password, (3) failed retry with decoded password, (4) user declining the retry offer, and (5) edge cases like malformed encoding or passwords with legitimate percent sequences.
| password && /%[0-9A-Fa-f]{2}/.test(password) && decodedPassword && password !== decodedPassword; | ||
|
|
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The retry logic doesn't verify that authMethod is specifically NativeAuth before offering to decode the password. While the condition on line 259 checks that username and password exist (which implicitly suggests native auth), it's possible for other auth methods to have these values set. Consider adding an explicit check authMethod === AuthMethodId.NativeAuth to the shouldOfferRetry condition on line 244 to ensure this feature only applies to native authentication where passwords are actually used.
| password && /%[0-9A-Fa-f]{2}/.test(password) && decodedPassword && password !== decodedPassword; | |
| authMethod === AuthMethodId.NativeAuth && | |
| password && | |
| /%[0-9A-Fa-f]{2}/.test(password) && | |
| decodedPassword && | |
| password !== decodedPassword; |
@tnaum-ms Thank you for the detailed review! I will try to make the changes you suggested by the end of this week. |
This PR addresses an issue where users often provide URL-encoded passwords leading to authentication failures. The logic waits for a connection failure after which:
decodeURIComponentmethod and regex search using/%[0-9A-Fa-f]{2}/.Files Changed:
DocumentDBClusterItem.ts: UpdatedauthenticateAndConnectcatch block to include the described logic after a connection failure.Testing performed:
I manually tested the workflow for an URL encoded password and a non URL encoded password. I tested both for a successful connection and an unsuccessful connection in the "retry with decoded password" logic.
The overall logic seems to have improved the User Experience.