Run the browser-based CF Access auth flow. 1. Generates a one-time confirmation code 2. Starts an ephemeral localhost HTTP server 3. Opens the browser to the gateway's `/auth/connect` page 4. Waits for the XHR POST callback with the CF JWT and matching code 5. Returns the token
(gateway_endpoint: &str)
| 89 | /// 4. Waits for the XHR POST callback with the CF JWT and matching code |
| 90 | /// 5. Returns the token |
| 91 | pub async fn browser_auth_flow(gateway_endpoint: &str) -> Result<String> { |
| 92 | // Short-circuit when the browser is suppressed (CI, e2e tests, headless |
| 93 | // environments). Without this early return the function still binds a TCP |
| 94 | // listener, spawns a callback server, and waits the full AUTH_TIMEOUT |
| 95 | // (120 s) for a POST that will never arrive. |
| 96 | let no_browser = std::env::var("OPENSHELL_NO_BROWSER") |
| 97 | .is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true")); |
| 98 | if no_browser { |
| 99 | return Err(miette::miette!( |
| 100 | "authentication skipped (OPENSHELL_NO_BROWSER is set).\n\ |
| 101 | Authenticate later with: openshell gateway login" |
| 102 | )); |
| 103 | } |
| 104 | |
| 105 | #[cfg(test)] |
| 106 | if std::env::var("OPENSHELL_TEST_BROWSER_AUTH_FAIL") |
| 107 | .is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true")) |
| 108 | { |
| 109 | return Err(miette::miette!( |
| 110 | "browser authentication failed (OPENSHELL_TEST_BROWSER_AUTH_FAIL is set)" |
| 111 | )); |
| 112 | } |
| 113 | |
| 114 | let listener = TcpListener::bind("127.0.0.1:0").await.into_diagnostic()?; |
| 115 | let local_addr = listener.local_addr().into_diagnostic()?; |
| 116 | let callback_port = local_addr.port(); |
| 117 | |
| 118 | let code = generate_confirmation_code(); |
| 119 | |
| 120 | let auth_url = format!( |
| 121 | "{}/auth/connect?callback_port={callback_port}&code={code}", |
| 122 | gateway_endpoint.trim_end_matches('/') |
| 123 | ); |
| 124 | |
| 125 | // Channel to receive the token from the callback handler. |
| 126 | let (tx, rx) = oneshot::channel::<String>(); |
| 127 | |
| 128 | // Spawn the callback server. |
| 129 | let server_handle = tokio::spawn(run_callback_server( |
| 130 | listener, |
| 131 | tx, |
| 132 | code.clone(), |
| 133 | gateway_endpoint.to_string(), |
| 134 | )); |
| 135 | |
| 136 | // Prompt the user before opening the browser. |
| 137 | eprintln!(" Confirmation code: {code}"); |
| 138 | eprintln!(" Verify this code matches your browser before clicking Connect."); |
| 139 | eprintln!(); |
| 140 | |
| 141 | eprint!("Press Enter to open the browser for authentication..."); |
| 142 | std::io::stderr().flush().ok(); |
| 143 | let mut input = String::new(); |
| 144 | std::io::stdin().read_line(&mut input).ok(); |
| 145 | drop(input); |
| 146 | |
| 147 | if let Err(e) = open_browser_url(&auth_url) { |
| 148 | debug!(error = %e, "failed to open browser"); |
no test coverage detected