Skip to content

feat(IDE-1701): settings page auth flow — bridge persist and forward apiUrl#453

Merged
nick-y-snyk merged 12 commits intomainfrom
feat/generic-webview-execute-command-bridge
Mar 30, 2026
Merged

feat(IDE-1701): settings page auth flow — bridge persist and forward apiUrl#453
nick-y-snyk merged 12 commits intomainfrom
feat/generic-webview-execute-command-bridge

Conversation

@nick-y-snyk
Copy link
Copy Markdown
Contributor

@nick-y-snyk nick-y-snyk commented Mar 5, 2026

What & Why

The settings page drives authentication via snyk.login with [authMethod, endpoint, insecure] args. Before forwarding to the LS, the IDE must persist these values locally so they survive a page close. Additionally, $/snyk.hasAuthenticated now carries apiUrl which is forwarded to the HTML settings window so the settings page can update both fields after auth.

Bridge persist writes to option properties directly — no Save() call means SettingsChanged never fires, DidChangeConfigurationAsync is not triggered.

Changes

HtmlSettingsScriptingBridge.cs — bridge persist
In __ideExecuteCommand__, before dispatching snyk.login to the LS, when args.Length >= 3:

  • Sets Options.AuthenticationMethod (C# switch: "oauth"OAuth, "pat"Pat, "token"Token)
  • Sets Options.CustomEndpoint from args[1]
  • Sets Options.IgnoreUnknownCA from args[2]
  • No Save() call — properties written in-memory only, no SettingsChanged event, no DidChangeConfigurationAsync

SnykLanguageClientCustomTarget.csOnHasAuthenticated

  • Always saves CustomEndpoint (when non-empty) and ApiToken
  • Save(serviceProvider.Options, false)false suppresses SettingsChanged → no DidChangeConfigurationAsync loop
  • HtmlSettingsWindow.Instance?.UpdateAuthToken(token, apiUrl) — always updates webview so settings page shows both fields immediately
  • isNewLogin guard (string.IsNullOrEmpty(oldToken) && !string.IsNullOrEmpty(token)) gates HandleAuthenticationSuccess + scan — token refreshes skip scanning

HtmlSettingsWindow.xaml.cs
UpdateAuthToken(string token, string apiUrl = null) passes apiUrl through to window.setAuthToken(escapedToken, escapedApiUrl).

Tests

  • SnykLanguageClientCustomTargetTests.cs: new tests for new login (verifies endpoint + token saved, Save(_, false), HandleAuthenticationSuccess called), token refresh (quiet save, no HandleAuthenticationSuccess), and always-updates-webview cases
  • HtmlSettingsScriptingBridgeTest.cs (new): 7 tests — auth method mapping (oauth/pat/token), endpoint, insecure, no-op on <3 args, no-op on non-login command

Test plan

  • Settings page: change endpoint + auth method → click Authenticate → IDE saves values → auth succeeds → token and apiUrl appear in settings page
  • Close/reopen settings page → values persist
  • Token refresh → settings page webview shows new token without triggering a scan
  • No DidChangeConfigurationAsync fired when bridge saves auth params
  • Run test suite

…rotocol version to 25 [IDE-1701]

Replace __ideLogin__/__ideLogout__ COM methods with a generic
__ideExecuteCommand__ bridge that dispatches any LS command with
callback support via window.__ideCallbacks__. Bump ProtocolVersion to 25.
…use [IDE-1701]

Move BuildClientScript() and DispatchAsync() into a standalone ExecuteCommandBridge
class. HtmlSettingsScriptingBridge delegates dispatch and HtmlSettingsWindow
uses BuildClientScript() for injection, enabling any future WebBrowser panel
(e.g. tree view) to reuse the same bridge.
… flag

- Remove persist flag from OnHasAuthenticated — always saves endpoint + token
- Keep Save(options, false) to avoid DidChangeConfigurationAsync loop
- Keep isNewLogin guard — only triggers HandleAuthenticationSuccess on first login
- Keep UpdateAuthToken(token, apiUrl) — settings page webview always updated
- Add bridge persist in HtmlSettingsScriptingBridge.__ideExecuteCommand__: when
  snyk.login called with 3+ args, save authMethod/endpoint/ignoreUnknownCA to
  options properties directly (no Save() call → no SettingsChanged event →
  no DidChangeConfigurationAsync to LS)
- Remove OnHasAuthenticated_NoPersist test; rename persist-prefixed tests
- Add HtmlSettingsScriptingBridgeTest with 6 tests for login args persist
@nick-y-snyk nick-y-snyk requested review from a team as code owners March 5, 2026 17:46
@snyk-io
Copy link
Copy Markdown

snyk-io bot commented Mar 5, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

…ebview

Pass apiUrl alongside token when calling window.setAuthToken via InvokeSetAuthToken
so the settings page can update both the token and apiUrl fields after auth.
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 5, 2026 17:55 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 5, 2026 17:55 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk changed the title feat(IDE-1701): settings page auth flow — bridge persist and remove persist flag feat(IDE-1701): settings page auth flow — bridge persist and forward apiUrl Mar 5, 2026
- Add callbackId XSS allowlist guard (regex ^(__cb_\d+)?$) in ExecuteCommandBridge.IsValidCallbackId, used in DispatchAsync and InvokeCommandCallback
- Fix JS string escaping order in InvokeSetAuthToken: escape backslash before single-quote
- Add volatile keyword to HtmlSettingsWindow singleton instance for cross-thread visibility
- Clear stored token when auth method changes in ParseAndSaveConfigAsync to prevent stale token from one method being used with another
- Add tests for all four fixes
@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 19, 2026 17:57 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 19, 2026 17:57 — with GitHub Actions Inactive
@snyk-pr-review-bot

This comment has been minimized.

@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 19, 2026 18:14 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 19, 2026 18:14 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 19, 2026 18:14 — with GitHub Actions Inactive
@snyk-pr-review-bot

This comment has been minimized.

The LS HTML page checks window.__ideExecuteCommand__ during its own
initialization scripts. Injecting only in LoadCompleted (after the page
has already parsed and run its scripts) means the auth button is never
wired up, causing it to do nothing when clicked.

Now inject bridge functions directly into the HTML string before
NavigateToString so they are defined before any LS page scripts run.
The LoadCompleted injection is kept as a secondary safety net.
@nick-y-snyk nick-y-snyk force-pushed the feat/generic-webview-execute-command-bridge branch from f63c7b5 to af40ad6 Compare March 24, 2026 15:41
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 24, 2026 15:41 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 24, 2026 15:41 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 24, 2026 15:41 — with GitHub Actions Inactive
@snyk-pr-review-bot

This comment has been minimized.

Prevents XSS-to-arbitrary-command escalation by rejecting any command
not prefixed with "snyk." before it reaches the Language Server.
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 26, 2026 10:45 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 26, 2026 10:45 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 26, 2026 10:45 — with GitHub Actions Inactive
@snyk-pr-review-bot

This comment has been minimized.

var script = BuildIdeBridgeScript();
var scriptTag = $"<script type=\"text/javascript\">{script}</script>";

var headIndex = html.IndexOf("<head>", StringComparison.OrdinalIgnoreCase);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny nitpick: "" should probably be extracted to a variable as we have two places (here and line 193) where we rely on the string's exact contents.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly necessary in this case given the close proximity of the two use cases, but I think it's good practice.

doc.GetElementsByTagName("head")[0].AppendChild(scriptElement);
});
}
catch (Exception ex)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for wrapping the run in a try/catch instead of the other way around?

}

var escapedToken = token.Replace("'", "\\'").Replace("\"", "\\\"");
var escapedToken = token.Replace("\\", "\\\\").Replace("'", "\\'");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as the PRs for the other IDEs; might be worth extracting the escape logic into a utility function.

[InlineData("__cb_abc")]
[InlineData(";drop table--")]
[InlineData("__cb_1; alert(1)")]
[InlineData("__cb_")]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might also be worth adding a line for "_cb_8" (i.e, with only a single leading underscore)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, a blank but non-empty string: " ".

}

[Fact]
public void IdeExecuteCommand_SnykLogin_SavesOAuthMethod()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test for the default (e.g, checking that we set oauth when an incorrect, blank, or empty string is passed)

- Extract <head> tag string to constant in InjectBridgeScriptIntoHtml
- Move callbackId validation outside ThreadHelper.Run in InvokeCommandCallback
- Extract JS string escaping into ExecuteCommandBridge.EscapeForJsString utility
- Add _cb_8 and whitespace test cases for IsValidCallbackId
- Add test for default OAuth fallback on invalid auth method string
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 30, 2026 09:56 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 30, 2026 09:56 — with GitHub Actions Inactive
@nick-y-snyk nick-y-snyk temporarily deployed to snyk-msbuild-envs March 30, 2026 09:56 — with GitHub Actions Inactive
@snyk-pr-review-bot
Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Volatile Configuration State 🟠 [major]

In __ideExecuteCommand__, when the command is snyk.login, the code updates Options.AuthenticationMethod, Options.CustomEndpoint, and Options.IgnoreUnknownCA in-memory but does not call OptionsManager.Save(). If the user successfully authenticates via the webview but then closes the settings window without clicking 'Save', these critical connection settings will be lost on the next IDE restart, even though the authentication token itself was persisted in OnHasAuthenticated via SnykLanguageClientCustomTarget.

    serviceProvider.Options.AuthenticationMethod = authMethodStr switch
    {
        "oauth" => AuthenticationType.OAuth,
        "pat" => AuthenticationType.Pat,
        "token" => AuthenticationType.Token,
        _ => AuthenticationType.OAuth,
    };
    serviceProvider.Options.CustomEndpoint = args[1]?.ToString() ?? string.Empty;
    serviceProvider.Options.IgnoreUnknownCA = args[2] is bool b ? b : Convert.ToBoolean(args[2]);
}
Brittle New Login Check 🟡 [minor]

The isNewLogin check in OnHasAuthenticated uses string.IsNullOrEmpty(oldToken). If the existing token is a non-empty but invalid placeholder (e.g., from a corrupted settings file or a failed previous migration), isNewLogin will be false. This prevents HandleAuthenticationSuccess and the subsequent initial scan from running, leaving the extension in a 'logged in' state without actually having performed an initial scan or updating the UI properly.

var isNewLogin = string.IsNullOrEmpty(oldToken) && !string.IsNullOrEmpty(token);
📚 Repository Context Analyzed

This review considered 80 relevant code sections from 12 files (average relevance: 0.67)

@nick-y-snyk nick-y-snyk merged commit 4c4d717 into main Mar 30, 2026
14 checks passed
@nick-y-snyk nick-y-snyk deleted the feat/generic-webview-execute-command-bridge branch March 30, 2026 10:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants