package oidc import ( "fmt" "os" "os/exec" "runtime" "strings" ) // openBrowser tries, in order: an explicit command override, $BROWSER, then // platform-appropriate auto-detection. It exists instead of // github.com/pkg/browser because that package shells out to xdg-open, which // is absent on a bare WSL2 install (verified: no xdg-open, no // xclip/wl-copy, no wslview on this project's own dev machine) — there, // the working option is handing the URL to the Windows side via // powershell.exe or cmd.exe. // // override, when non-empty, is treated as a command template: "%s" is // replaced with the URL if present, otherwise the URL is appended as the // final argument. It is run through the shell so users can supply // something like `firefox --new-tab %s`. func openBrowser(override, url string) error { if override != "" { return runShell(substituteOrAppend(override, url)) } if b := os.Getenv("BROWSER"); b != "" { return runShell(substituteOrAppend(b, url)) } for _, cand := range candidates(url) { cmd := exec.Command(cand[0], cand[1:]...) if err := cmd.Start(); err == nil { // Don't Wait(): a real browser backgrounds itself, and waiting on // e.g. `cmd.exe /c start` (which returns immediately anyway) is // harmless, but waiting on something that stays foregrounded // would block the login flow. return nil } } return fmt.Errorf("no working browser launcher found for %s (tried: %s)", runtime.GOOS, candidateNames(url)) } // candidates returns launcher argv lists to try in order, most-specific // (and most likely to actually work on this host) first. func candidates(url string) [][]string { var out [][]string if isWSL() { out = append(out, []string{"wslview", url}, []string{"powershell.exe", "-NoProfile", "-NonInteractive", "-Command", "Start-Process", quoteForPowerShell(url)}, []string{"cmd.exe", "/c", "start", "", url}, ) } switch runtime.GOOS { case "darwin": out = append(out, []string{"open", url}) case "windows": out = append(out, []string{"cmd", "/c", "start", "", url}, []string{"rundll32", "url.dll,FileProtocolHandler", url}) default: out = append(out, []string{"xdg-open", url}, []string{"x-www-browser", url}) } return out } func candidateNames(url string) string { var names []string for _, c := range candidates(url) { names = append(names, c[0]) } return strings.Join(names, ", ") } // isWSL detects WSL1/2 by checking /proc/version for the "microsoft" // marker Microsoft's kernel build injects there — the standard, widely // used detection technique since there's no dedicated syscall for it. func isWSL() bool { if runtime.GOOS != "linux" { return false } b, err := os.ReadFile("/proc/version") if err != nil { return false } v := strings.ToLower(string(b)) return strings.Contains(v, "microsoft") || strings.Contains(v, "wsl") } func substituteOrAppend(template, url string) string { if strings.Contains(template, "%s") { return fmt.Sprintf(template, url) } return template + " " + url } func runShell(command string) error { cmd := exec.Command("sh", "-c", command) return cmd.Start() } // quoteForPowerShell wraps url in single quotes for use inside a // -Command argument; OIDC auth URLs are server-generated and may contain // characters PowerShell would otherwise interpret. func quoteForPowerShell(url string) string { return "'" + strings.ReplaceAll(url, "'", "''") + "'" }