Skip to content

Commit 9088d98

Browse files
lewingCopilot
andcommitted
Skip browser flow only when WSL has no Windows-browser launcher
On WSL the interactive browser flow only succeeds when: 1. A Windows-side launcher is available (wslu's `wslview` is the canonical bridge), and 2. WSL2 localhost forwarding can route the OAuth redirect back into the WSL network namespace (default NAT mode handles this). When wslview is present the flow works end-to-end and benefits from Windows session SSO (which is what gets past Conditional Access). When wslview is absent, xdg-open silently does nothing and InteractiveBrowserCredential.Authenticate() blocks forever waiting for a redirect that will never arrive — in that specific case we skip straight to device code so the user at least sees a code instead of an indefinite hang. Env var overrides: DARC_FORCE_BROWSER_AUTH=1 always attempt browser flow DARC_USE_DEVICE_CODE=1 always skip browser flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bb914a3 commit 9088d98

1 file changed

Lines changed: 71 additions & 0 deletions

File tree

src/Maestro/Maestro.Common/AppCredentials/CachedInteractiveBrowserCredential.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,77 @@ public CachedInteractiveBrowserCredential(
5858
return Task.CompletedTask;
5959
},
6060
});
61+
62+
// On WSL the interactive browser flow only succeeds when a Windows-side browser
63+
// launcher (wslu's `wslview`) is installed AND WSL2 localhost forwarding can route
64+
// the OAuth redirect back into the WSL network namespace. With wslu present that
65+
// path works on default NAT-mode WSL2. Without wslu, xdg-open silently does nothing
66+
// and `_browserCredential.Authenticate()` blocks forever waiting for a redirect
67+
// that will never arrive. In that specific case, skip straight to device code so
68+
// the user at least sees a code rather than an indefinite hang.
69+
if (IsWslWithoutBrowserLauncher())
70+
{
71+
Interlocked.Exchange(ref _isDeviceCodeFallback, 1);
72+
}
73+
}
74+
75+
private static bool IsWslWithoutBrowserLauncher()
76+
{
77+
// Opt-out: user knows their setup supports browser auth even if we don't detect it.
78+
if (string.Equals(Environment.GetEnvironmentVariable("DARC_FORCE_BROWSER_AUTH"), "1", StringComparison.Ordinal))
79+
{
80+
return false;
81+
}
82+
83+
// Opt-in: user explicitly wants device code regardless of environment.
84+
if (string.Equals(Environment.GetEnvironmentVariable("DARC_USE_DEVICE_CODE"), "1", StringComparison.Ordinal))
85+
{
86+
return true;
87+
}
88+
89+
bool isWsl = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WSL_DISTRO_NAME"))
90+
|| !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WSL_INTEROP"));
91+
if (!isWsl)
92+
{
93+
return false;
94+
}
95+
96+
// On WSL: only skip browser flow if no usable launcher is on PATH.
97+
// wslview (from the wslu package) is the canonical Windows-browser bridge.
98+
// BROWSER env var override is also respected by Azure.Identity's launcher.
99+
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BROWSER")))
100+
{
101+
return false;
102+
}
103+
return !ExistsOnPath("wslview");
104+
}
105+
106+
private static bool ExistsOnPath(string executable)
107+
{
108+
var path = Environment.GetEnvironmentVariable("PATH");
109+
if (string.IsNullOrEmpty(path))
110+
{
111+
return false;
112+
}
113+
foreach (var dir in path.Split(Path.PathSeparator))
114+
{
115+
if (string.IsNullOrEmpty(dir))
116+
{
117+
continue;
118+
}
119+
try
120+
{
121+
if (File.Exists(Path.Combine(dir, executable)))
122+
{
123+
return true;
124+
}
125+
}
126+
catch
127+
{
128+
// Ignore inaccessible PATH entries.
129+
}
130+
}
131+
return false;
61132
}
62133

63134
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)

0 commit comments

Comments
 (0)