Skip to content

Commit

Permalink
implement vercel auth token for SSO
Browse files Browse the repository at this point in the history
  • Loading branch information
Zertsov committed Jan 31, 2024
1 parent d678ab3 commit dd9c374
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 24 deletions.
12 changes: 1 addition & 11 deletions crates/turborepo-auth/src/auth/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,13 @@ use tracing::warn;
use turborepo_api_client::Client;
use turborepo_ui::{start_spinner, BOLD, UI};

use crate::{error, server::LoginServer, ui};
use crate::{error, server::LoginServer, ui, Token};

const DEFAULT_HOST_NAME: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 9789;
const VERCEL_CONFIG_DIR: &str = "com.vercel.cli";
const VERCEL_TOKEN_FILE: &str = "auth.json";

/// Token is the result of a successful login. It contains the token string and
/// a boolean indicating whether the token already existed on the filesystem.
pub struct Token {
/// The actual token string.
pub token: String,
/// If this is `true`, it means this token already exists on the filesystem.
/// If `false`, this is a new token.
pub is_existing: bool,
}

/// Login writes a token to disk at token_path. If a token is already present,
/// we do not overwrite it and instead log that we found an existing token.
pub async fn login<'a>(
Expand Down
62 changes: 50 additions & 12 deletions crates/turborepo-auth/src/auth/sso.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
use std::{borrow::Cow, sync::Arc};
use std::sync::Arc;

use reqwest::Url;
use tokio::sync::OnceCell;
use tracing::warn;
use turborepo_api_client::Client;
use turborepo_ui::{start_spinner, BOLD, UI};

use crate::{error, server, ui, Error};
use crate::{error, server, ui, Error, Token};

const DEFAULT_HOST_NAME: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 9789;
const DEFAULT_SSO_PROVIDER: &str = "SAML/OIDC Single Sign-On";
const VERCEL_CONFIG_DIR: &str = "com.vercel.cli";
const VERCEL_TOKEN_FILE: &str = "auth.json";

fn make_token_name() -> Result<String, Error> {
let host = hostname::get().map_err(Error::FailedToMakeSSOTokenName)?;
Expand All @@ -21,16 +23,17 @@ fn make_token_name() -> Result<String, Error> {
))
}

/// present, and the token has access to the provided `sso_team`, we do not
/// overwrite it and instead log that we found an existing token.
/// Perform an SSO login flow. If an existing token is present, and the token
/// has access to the provided `sso_team`, we do not overwrite it and instead
/// log that we found an existing token.
pub async fn sso_login<'a>(
api_client: &impl Client,
ui: &UI,
existing_token: Option<&'a str>,
login_url_configuration: &str,
sso_team: &str,
login_server: &impl server::SSOLoginServer,
) -> Result<Cow<'a, str>, Error> {
) -> Result<Token, Error> {
// Check if token exists first. Must be there for the user and contain the
// sso_team passed into this function.
if let Some(token) = existing_token {
Expand All @@ -45,7 +48,40 @@ pub async fn sso_login<'a>(
{
println!("{}", ui.apply(BOLD.apply_to("Existing token found!")));
ui::print_cli_authorized(&response_user.user.email, ui);
return Ok(token.into());
return Ok(Token {
token: token.into(),
is_existing: true,
});
}
}
}

// No existing token found. If the user is logging into Vercel, check for an
// existing `vc` token with correct scope.
if login_url_configuration.contains("vercel.com") {
if let Some(vercel_config_dir) = turborepo_dirs::vercel_config_dir() {
let vercel_token_path = vercel_config_dir
.join(VERCEL_CONFIG_DIR)
.join(VERCEL_TOKEN_FILE);
#[derive(serde::Deserialize)]
struct VercelToken {
token: String,
}
let contents = std::fs::read_to_string(&vercel_token_path)?;
if let Ok(token) = serde_json::from_str::<VercelToken>(&contents) {
// extract the actual token out of the struct
let token = token.token;
if let Ok(response) = api_client.get_user(&token).await {
println!(
"{}",
ui.apply(BOLD.apply_to("Existing Vercel token found!"))
);
ui::print_cli_authorized(&response.user.email, ui);
return Ok(Token {
token,
is_existing: true,
});
}
}
}
}
Expand Down Expand Up @@ -95,7 +131,10 @@ pub async fn sso_login<'a>(

ui::print_cli_authorized(&user_response.user.email, ui);

Ok(verified_user.token.into())
Ok(Token {
token: verified_user.token,
is_existing: false,
})
}

#[cfg(test)]
Expand Down Expand Up @@ -307,9 +346,10 @@ mod tests {

let token = sso_login(&api_client, &ui, None, &url, team, &login_server)
.await
.unwrap();
.unwrap()
.token;

let got_token = Some(token.to_string());
let got_token = Some(token);

assert_eq!(got_token, Some(EXPECTED_VERIFICATION_TOKEN.to_owned()));

Expand All @@ -326,9 +366,7 @@ mod tests {
.await
.unwrap();

// We can confirm that we didn't fetch a new token because we're borrowing the
// existing token and not getting a new allocation.
assert!(second_token.is_borrowed());
assert!(second_token.is_existing);

handle.abort();

Expand Down
10 changes: 10 additions & 0 deletions crates/turborepo-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ mod ui;
pub use auth::*;
pub use error::Error;
pub use server::*;

/// Token is the result of a successful login. It contains the token string and
/// a boolean indicating whether the token already existed on the filesystem.
pub struct Token {
/// The actual token string.
pub token: String,
/// If this is `true`, it means this token already exists on the filesystem.
/// If `false`, this is a new token.
pub is_existing: bool,
}
3 changes: 2 additions & 1 deletion crates/turborepo-lib/src/commands/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ pub async fn sso_login(
sso_team,
&DefaultSSOLoginServer,
)
.await?;
.await?
.token;

let global_config_path = base.global_config_path()?;
let before = global_config_path
Expand Down

0 comments on commit dd9c374

Please sign in to comment.