Skip to content

Commit

Permalink
feat(turborepo-auth): create new crate to handle authentication to ba…
Browse files Browse the repository at this point in the history
…ckend (#6027)

This creates a `turborepo-auth` crate that contains the logic for
logging
into a backend (Vercel by default). It only covers logging into non-SAML
enforced
teams to get a proof of concept merged, sso_login and logout will be
moved in
the future.
  • Loading branch information
mehulkar authored and Zertsov committed Sep 27, 2023
1 parent 57f8bd5 commit c79e111
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 159 deletions.
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions crates/turborepo-auth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "turborepo-auth"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"

[dependencies]
anyhow.workspace = true
axum-server = { workspace = true }
axum.workspace = true
reqwest.workspace = true
serde.workspace = true
thiserror = "1.0.38"
tokio.workspace = true
tracing.workspace = true
turborepo-api-client = { workspace = true }
turborepo-ui.workspace = true
turborepo-vercel-api-mock = { workspace = true }
webbrowser = { workspace = true }

[dev-dependencies]
port_scanner = { workspace = true }
184 changes: 184 additions & 0 deletions crates/turborepo-auth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#[cfg(not(test))]
use std::net::SocketAddr;
use std::sync::Arc;

use anyhow::{anyhow, Result};
#[cfg(not(test))]
use axum::{extract::Query, response::Redirect, routing::get, Router};
use reqwest::Url;
use serde::Deserialize;
use tokio::sync::OnceCell;
#[cfg(not(test))]
use tracing::warn;
use turborepo_api_client::APIClient;
use turborepo_ui::{start_spinner, BOLD, CYAN, UI};

const DEFAULT_HOST_NAME: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 9789;

use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error(
"loginUrl is configured to \"{value}\", but cannot be a base URL. This happens in \
situations like using a `data:` URL."
)]
LoginUrlCannotBeABase { value: String },
}

pub async fn login<F>(
api_client: APIClient,
ui: &UI,
mut set_token: F,
login_url_configuration: &str,
) -> Result<()>
where
F: FnMut(&str) -> Result<()>,
{
let redirect_url = format!("http://{DEFAULT_HOST_NAME}:{DEFAULT_PORT}");
let mut login_url = Url::parse(login_url_configuration)?;

login_url
.path_segments_mut()
.map_err(|_: ()| Error::LoginUrlCannotBeABase {
value: login_url_configuration.to_string(),
})?
.extend(["turborepo", "token"]);

login_url
.query_pairs_mut()
.append_pair("redirect_uri", &redirect_url);

println!(">>> Opening browser to {login_url}");
let spinner = start_spinner("Waiting for your authorization...");
direct_user_to_url(login_url.as_str());

let token_cell = Arc::new(OnceCell::new());
run_login_one_shot_server(
DEFAULT_PORT,
login_url_configuration.to_string(),
token_cell.clone(),
)
.await?;

spinner.finish_and_clear();

let token = token_cell
.get()
.ok_or_else(|| anyhow!("Failed to get token"))?;

// This function is passed in from turborepo-lib
// TODO: inline this here and only pass in the location to write the token as an
// optional arg.
set_token(token)?;

// TODO: make this a request to /teams endpoint instead?
let user_response = api_client.get_user(token.as_str()).await?;

println!(
"
{} Turborepo CLI authorized for {}
{}
{}
",
ui.rainbow(">>> Success!"),
user_response.user.email,
ui.apply(
CYAN.apply_to("To connect to your Remote Cache, run the following in any turborepo:")
),
ui.apply(BOLD.apply_to(" npx turbo link"))
);
Ok(())
}

// TODO: Duplicated
#[cfg(test)]
fn direct_user_to_url(_: &str) {}
#[cfg(not(test))]
fn direct_user_to_url(url: &str) {
if webbrowser::open(url).is_err() {
warn!("Failed to open browser. Please visit {url} in your browser.");
}
}

#[derive(Debug, Clone, Deserialize)]
struct LoginPayload {
#[cfg(not(test))]
token: String,
}

#[cfg(test)]
async fn run_login_one_shot_server(
_: u16,
_: String,
login_token: Arc<OnceCell<String>>,
) -> Result<()> {
login_token
.set(turborepo_vercel_api_mock::EXPECTED_TOKEN.to_string())
.unwrap();
Ok(())
}

#[cfg(not(test))]
async fn run_login_one_shot_server(
port: u16,
login_url_base: String,
login_token: Arc<OnceCell<String>>,
) -> Result<()> {
let handle = axum_server::Handle::new();
let route_handle = handle.clone();
let app = Router::new()
// `GET /` goes to `root`
.route(
"/",
get(|login_payload: Query<LoginPayload>| async move {
let _ = login_token.set(login_payload.0.token);
route_handle.shutdown();
Redirect::to(&format!("{login_url_base}/turborepo/success"))
}),
);
let addr = SocketAddr::from(([127, 0, 0, 1], port));

Ok(axum_server::bind(addr)
.handle(handle)
.serve(app.into_make_service())
.await?)
}

#[cfg(test)]
mod test {
use port_scanner;
use tokio;
use turborepo_ui::UI;
use turborepo_vercel_api_mock::start_test_server;

use crate::login;

#[tokio::test]
async fn test_login() {
let port = port_scanner::request_open_port().unwrap();
let api_server = tokio::spawn(start_test_server(port));

let ui = UI::new(false);

let url = format!("http://localhost:{port}");

let api_client =
turborepo_api_client::APIClient::new(url.clone(), 1000, "1", false).unwrap();

// closure that will check that the token is sent correctly
let mut got_token = String::new();
let set_token = |t: &str| -> Result<(), anyhow::Error> {
got_token = t.to_string();
Ok(())
};

login(api_client, &ui, set_token, &url).await.unwrap();

api_server.abort();
assert_eq!(got_token, turborepo_vercel_api_mock::EXPECTED_TOKEN);
}
}
1 change: 1 addition & 0 deletions crates/turborepo-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ tokio-util = { version = "0.7.7", features = ["compat"] }
tonic = { version = "0.8.3", features = ["transport"] }
tonic-reflection = { version = "0.6.0", optional = true }
tower = "0.4.13"
turborepo-auth = { path = "../turborepo-auth" }
turborepo-fs = { path = "../turborepo-fs" }
turborepo-vercel-api = { path = "../turborepo-vercel-api" }
uds_windows = "1.0.2"
Expand Down
Loading

0 comments on commit c79e111

Please sign in to comment.