Store credentials in the keychain on login

Max Brunsfeld created

Change summary

zed/src/lib.rs | 135 +++++++++++++++++++++++++++++----------------------
1 file changed, 77 insertions(+), 58 deletions(-)

Detailed changes

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")?;
+    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()));
+            }
 
-                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
-                    );
+            // 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);
                 }
-
-                platform.activate(true);
-                return Ok(());
             }
-        }
-        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();
+            // 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) {