From ee52a5d06189ae4b67f99e97dbb684a5721927b8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 8 Jun 2021 18:08:09 -0700 Subject: [PATCH] Store credentials in the keychain on login --- zed/src/lib.rs | 139 ++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 60 deletions(-) diff --git a/zed/src/lib.rs b/zed/src/lib.rs index d40cedf480..366a13fdbc 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -1,8 +1,7 @@ -use std::convert::TryInto; - use anyhow::{anyhow, Context}; use gpui::MutableAppContext; use smol::io::{AsyncBufReadExt, AsyncWriteExt}; +use std::convert::TryFrom; use url::Url; pub mod assets; @@ -35,66 +34,86 @@ fn authenticate(_: &(), cx: &mut MutableAppContext) { let zed_url = std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string()); let platform = cx.platform().clone(); - let task = cx.background_executor().spawn(async move { - let listener = smol::net::TcpListener::bind("127.0.0.1:0").await?; - let port = listener.local_addr()?.port(); - - let (public_key, private_key) = - zed_rpc::auth::keypair().expect("failed to generate keypair for auth"); - - let public_key_string: String = public_key.try_into().unwrap(); - - platform.open_url(&format!( - "{}/sign_in?native_app_port={}&native_app_public_key={}", - zed_url, port, public_key_string - )); - - let (mut stream, _) = listener.accept().await?; - let mut reader = smol::io::BufReader::new(&mut stream); - let mut line = String::new(); - reader.read_line(&mut line).await?; - - let mut parts = line.split(" "); - if parts.next() == Some("GET") { - if let Some(path) = parts.next() { - let url = Url::parse(&format!("http://example.com{}", path)) - .context("failed to parse login notification url")?; - let mut user_id = None; - let mut access_token = None; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value); - } else if key == "user_id" { - user_id = Some(value); - } - } - stream - .write_all(LOGIN_RESPONSE.as_bytes()) - .await - .context("failed to write login response")?; - stream.flush().await.context("failed to flush tcp stream")?; - - if let Some((user_id, access_token)) = user_id.zip(access_token) { - let access_token = private_key.decrypt_string(&access_token); - eprintln!( - "logged in. user_id: {}, access_token: {:?}", - user_id, access_token - ); - } - - platform.activate(true); - return Ok(()); + cx.background_executor() + .spawn(async move { + if let Some((user_id, access_token)) = platform.read_credentials(&zed_url) { + log::info!("already signed in. user_id: {}", user_id); + return Ok((user_id, String::from_utf8(access_token).unwrap())); } - } - Err(anyhow!("failed to parse http request from zed web app")) - }); - cx.spawn(|_| async move { - if let Err(e) = task.await { - log::error!("failed to login {:?}", e) - } - }) - .detach(); + // Generate a pair of asymmetric encryption keys. The public key will be used by the + // zed server to encrypt the user's access token, so that it can'be intercepted by + // any other app running on the user's device. + let (public_key, private_key) = + zed_rpc::auth::keypair().expect("failed to generate keypair for auth"); + let public_key_string = + String::try_from(public_key).expect("failed to serialize public key for auth"); + + // Listen on an open TCP port. This port will be used by the web browser to notify the + // application that the login is complete, and to send the user's id and access token. + let listener = smol::net::TcpListener::bind("127.0.0.1:0").await?; + let port = listener.local_addr()?.port(); + + // Open the Zed sign-in page in the user's browser, with query parameters that indicate + // that the user is signing in from a Zed app running on the same device. + platform.open_url(&format!( + "{}/sign_in?native_app_port={}&native_app_public_key={}", + zed_url, port, public_key_string + )); + + // Receive the HTTP request from the user's browser. Parse the first line, which contains + // the HTTP method and path. + let (mut stream, _) = listener.accept().await?; + let mut reader = smol::io::BufReader::new(&mut stream); + let mut line = String::new(); + reader.read_line(&mut line).await?; + let mut parts = line.split(" "); + let http_method = parts.next(); + if http_method != Some("GET") { + return Err(anyhow!( + "unexpected http method {:?} in request from zed web app", + http_method + )); + } + let path = parts.next().ok_or_else(|| { + anyhow!("failed to parse http request from zed login redirect - missing path") + })?; + + // Parse the query parameters from the HTTP request. + let mut user_id = None; + let mut access_token = None; + let url = Url::parse(&format!("http://example.com{}", path)) + .context("failed to parse login notification url")?; + for (key, value) in url.query_pairs() { + if key == "access_token" { + access_token = Some(value); + } else if key == "user_id" { + user_id = Some(value); + } + } + + // Write an HTTP response to the user's browser, instructing it to close the tab. + // Then transfer focus back to the application. + stream + .write_all(LOGIN_RESPONSE.as_bytes()) + .await + .context("failed to write login response")?; + stream.flush().await.context("failed to flush tcp stream")?; + platform.activate(true); + + // If login succeeded, then store the credentials in the keychain. + let user_id = user_id.ok_or_else(|| anyhow!("missing user_id in login request"))?; + let access_token = + access_token.ok_or_else(|| anyhow!("missing access_token in login request"))?; + let access_token = private_key + .decrypt_string(&access_token) + .context("failed to decrypt access token")?; + platform.write_credentials(&zed_url, &user_id, access_token.as_bytes()); + log::info!("successfully signed in. user_id: {}", user_id); + + Ok((user_id.to_string(), access_token)) + }) + .detach(); } fn quit(_: &(), cx: &mut MutableAppContext) {