Skip to content

Commit

Permalink
feat: add auth refresh command (#443)
Browse files Browse the repository at this point in the history
  • Loading branch information
morgante authored Aug 5, 2024
1 parent f79bc30 commit 3681506
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 22 deletions.
49 changes: 44 additions & 5 deletions crates/auth/src/auth0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use serde::Serialize;
use std::time::Instant;
use tokio::time::{interval, Duration};

use crate::info::AuthInfo;

lazy_static! {
pub static ref AUTH0_API_AUDIENCE: String = String::from("https://api2.grit.io");

Expand Down Expand Up @@ -49,9 +51,9 @@ struct AuthTokenResponsePending {
#[derive(Debug, Deserialize, Serialize, Clone)]
#[allow(dead_code)]
pub struct AuthTokenResponseSuccess {
pub access_token: String,
pub(crate) access_token: String,
/// The refresh token, which can be used to obtain new access tokens using the same authorization grant
refresh_token: Option<String>,
pub(crate) refresh_token: Option<String>,
}

#[derive(Deserialize, Debug)]
Expand Down Expand Up @@ -84,9 +86,9 @@ impl AuthSession {
&self.init_request.verification_uri_complete
}

pub fn token(&self) -> Result<&str> {
match &self.token_response {
Some(token) => Ok(token.access_token.as_str()),
pub fn token(self) -> Result<AuthInfo> {
match self.token_response {
Some(token) => Ok(AuthInfo::from(token)),
None => Err(anyhow::anyhow!(
"No Grit token available, please run grit auth login"
)),
Expand Down Expand Up @@ -140,6 +142,7 @@ impl AuthSession {
}
AuthTokenResponse::Success(success) => {
self.token_response = Some(success);

break;
}
}
Expand Down Expand Up @@ -173,3 +176,39 @@ pub async fn start_auth() -> Result<AuthSession> {

Ok(AuthSession::new(body))
}

/// Refreshes an AuthInfo using the refresh token.
pub async fn refresh_token(auth_info: &AuthInfo) -> Result<AuthInfo> {
if auth_info.refresh_token.is_none() {
return Err(anyhow::anyhow!("No refresh token available"));
}

let client = reqwest::Client::new();

let params = [
("grant_type", "refresh_token"),
("client_id", AUTH0_CLI_CLIENT_ID.as_str()),
("refresh_token", auth_info.refresh_token.as_ref().unwrap()),
];

let res = client
.post(format!(
"https://{}/oauth/token",
AUTH0_TENANT_DOMAIN.as_str(),
))
.header("Content-Type", "application/x-www-form-urlencoded")
.form(&params)
.send()
.await?;

let body = res.json::<AuthTokenResponseSuccess>().await?;

let mut info = AuthInfo::from(body);

// If the new response doesn't include a refresh token, use the old one
if info.refresh_token.is_none() {
info.refresh_token = auth_info.refresh_token.clone();
}

Ok(info)
}
16 changes: 15 additions & 1 deletion crates/auth/src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ use marzano_util::base64::decode_to_string;
#[derive(Clone, Debug)]
pub struct AuthInfo {
pub access_token: String,
pub refresh_token: Option<String>,
}

impl From<crate::auth0::AuthTokenResponseSuccess> for AuthInfo {
fn from(auth: crate::auth0::AuthTokenResponseSuccess) -> Self {
Self {
access_token: auth.access_token,
refresh_token: auth.refresh_token,
}
}
}

#[derive(serde::Deserialize, Debug)]
Expand All @@ -24,7 +34,10 @@ struct AuthInfoPayload {

impl AuthInfo {
pub fn new(access_token: String) -> Self {
Self { access_token }
Self {
access_token,
refresh_token: None,
}
}

fn get_payload(&self) -> Result<AuthInfoPayload> {
Expand Down Expand Up @@ -84,6 +97,7 @@ mod tests {
let jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwczovL2hhc3VyYS5pby9qd3QvY2xhaW1zIjp7IngtaGFzdXJhLWRlZmF1bHQtcm9sZSI6InVzZXIiLCJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbInVzZXIiXSwieC1oYXN1cmEtdXNlci1pZCI6ImdpdGh1YnwxNjI3ODAxIiwieC1oYXN1cmEtcmF3LW5pY2tuYW1lIjoibW9yZ2FudGUiLCJ4LWhhc3VyYS11c2VyLXRlbmFudCI6ImdpdGh1YiIsIngtaGFzdXJhLWF1dGgtcHJvdmlkZXIiOiJnaXRodWIiLCJ4LWhhc3VyYS11c2VyLW5pY2tuYW1lIjoiZ2l0aHVifG1vcmdhbnRlIn0sImlzcyI6Imh0dHBzOi8vYXV0aDAuZ3JpdC5pby8iLCJzdWIiOiJnaXRodWJ8MTYyNzgwMSIsImF1ZCI6Imh0dHBzOi8vYXBpMi5ncml0LmlvIiwiaWF0IjoxNzE4NzI2MzUzLCJleHAiOjE3MTg4MTI3NTN9.eEU0bSldfdxuWpXAKfWAuJBqTMR5BAdnAEhFu-hVlI4";
let auth_info = AuthInfo {
access_token: jwt.to_string(),
refresh_token: None,
};

match auth_info.get_user_name() {
Expand Down
2 changes: 2 additions & 0 deletions crates/auth/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ fn get_existing_token() -> Result<AuthInfo> {
let existing_token = get_config_var("API_TESTING_TOKEN")?;
let info = AuthInfo {
access_token: existing_token,
refresh_token: None,
};

if info.is_expired()? {
Expand Down Expand Up @@ -72,6 +73,7 @@ fn get_new_tokens() -> Result<AuthInfo> {

Ok(AuthInfo {
access_token: body.access_token,
refresh_token: None,
})
}

Expand Down
3 changes: 3 additions & 0 deletions crates/cli/src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use serde::Serialize;

use super::auth_login::LoginArgs;
use super::auth_logout::LogoutArgs;
use super::auth_refresh::RefreshAuthArgs;
use super::auth_token::GetTokenArgs;

#[derive(Parser, Debug, Serialize)]
Expand All @@ -20,4 +21,6 @@ pub enum AuthCommands {
/// Get your grit.io token
#[clap(aliases = &["print-token"])]
GetToken(GetTokenArgs),
/// Refresh your grit.io auth (this will also happen automatically when your token expires)
Refresh(RefreshAuthArgs),
}
6 changes: 4 additions & 2 deletions crates/cli/src/commands/auth_login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ pub(crate) async fn run_login(_arg: LoginArgs) -> Result<()> {
// Wait for the user to complete the login process
session.poll().await?;

updater.save_token(session.token()?).await?;
let token = session.token()?;

updater.save_token(&token).await?;

log::info!("You are now logged in!");

debug!("Token is: {}", session.token()?);
debug!("Token is: {:?}", token.access_token);

Ok(())
}
27 changes: 27 additions & 0 deletions crates/cli/src/commands/auth_refresh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use anyhow::Result;
use clap::Args;
use colored::Colorize;
use log::info;
use serde::Serialize;

use crate::updater::Updater;

#[derive(Args, Debug, Serialize)]
pub struct RefreshAuthArgs {}

pub(crate) async fn run_refresh_auth(_arg: RefreshAuthArgs) -> Result<()> {
let mut updater = Updater::from_current_bin().await?;

let auth = updater.refresh_auth().await?;

if let Some(username) = auth.get_user_name()? {
info!(
"Hello {}, your token has been refreshed.",
username.yellow()
);
} else {
info!("Hello, your token has been refreshed.");
}

Ok(())
}
2 changes: 1 addition & 1 deletion crates/cli/src/commands/auth_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub(crate) async fn run_get_token(_arg: GetTokenArgs) -> Result<()> {
Some(auth) => {
if auth.is_expired()? {
bail!(
"Auth token expired: {}. Run grit auth login to refresh.",
"Auth token expired: {}. Run grit auth refresh to refresh.",
auth.get_expiry()?
);
}
Expand Down
4 changes: 4 additions & 0 deletions crates/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub(crate) mod apply_pattern;
pub(crate) mod auth;
pub(crate) mod auth_login;
pub(crate) mod auth_logout;
pub(crate) mod auth_refresh;
pub(crate) mod auth_token;

pub(crate) mod doctor;
Expand Down Expand Up @@ -106,6 +107,7 @@ use self::{
apply::run_apply,
auth_login::run_login,
auth_logout::run_logout,
auth_refresh::run_refresh_auth,
auth_token::run_get_token,
check::run_check,
doctor::run_doctor,
Expand Down Expand Up @@ -177,6 +179,7 @@ impl fmt::Display for Commands {
AuthCommands::Login(_) => write!(f, "auth login"),
AuthCommands::Logout(_) => write!(f, "auth logout"),
AuthCommands::GetToken(_) => write!(f, "auth get-token"),
AuthCommands::Refresh(_) => write!(f, "auth refresh"),
},
Commands::Install(_) => write!(f, "install"),
Commands::Init(_) => write!(f, "init"),
Expand Down Expand Up @@ -366,6 +369,7 @@ async fn run_command() -> Result<()> {
AuthCommands::Login(arg) => run_login(arg).await,
AuthCommands::Logout(arg) => run_logout(arg).await,
AuthCommands::GetToken(arg) => run_get_token(arg).await,
AuthCommands::Refresh(arg) => run_refresh_auth(arg).await,
},
Commands::Lsp(arg) => run_lsp(arg).await,
Commands::Install(arg) => run_install(arg).await,
Expand Down
50 changes: 40 additions & 10 deletions crates/cli/src/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use axoupdater::{AxoUpdater, ReleaseSource, ReleaseSourceType, Version};
use chrono::{DateTime, NaiveDateTime, Utc};
use colored::Colorize;
use futures_util::StreamExt;
use indicatif::ProgressBar;
use log::info;
use marzano_auth::info::AuthInfo;
use marzano_gritmodule::config::REPO_CONFIG_DIR_NAME;
Expand Down Expand Up @@ -142,6 +143,7 @@ struct Manifest {
last_checked_update: Option<NaiveDateTime>,
installation_id: Option<Uuid>,
access_token: Option<String>,
refresh_token: Option<String>,
}

async fn read_manifest(manifest_path: &PathBuf) -> Result<Manifest> {
Expand All @@ -167,6 +169,7 @@ pub struct Updater {
last_checked_update: Option<NaiveDateTime>,
pub installation_id: Uuid,
access_token: Option<String>,
refresh_token: Option<String>,
}

impl Updater {
Expand Down Expand Up @@ -197,6 +200,7 @@ impl Updater {
last_checked_update: manifest.last_checked_update,
installation_id: manifest.installation_id.unwrap_or_else(Uuid::new_v4),
access_token: manifest.access_token,
refresh_token: manifest.refresh_token,
});
}

Expand All @@ -214,6 +218,7 @@ impl Updater {
last_checked_update: None,
installation_id: Uuid::new_v4(),
access_token: None,
refresh_token: None,
};
Ok(updater)
}
Expand Down Expand Up @@ -427,15 +432,19 @@ impl Updater {
last_checked_update: self.last_checked_update,
installation_id: Some(self.installation_id),
access_token: self.access_token.clone(),
refresh_token: self.refresh_token.clone(),
};
let manifest_string = serde_json::to_string_pretty(&manifest)?;
manifest_file.write_all(manifest_string.as_bytes()).await?;
Ok(())
}

/// Save a new auth token to the manifest
pub async fn save_token(&mut self, token: &str) -> Result<()> {
self.access_token = Some(token.to_string());
pub async fn save_token(&mut self, auth: &AuthInfo) -> Result<()> {
self.access_token = Some(auth.access_token.clone());
if auth.refresh_token.is_some() {
self.refresh_token = auth.refresh_token.clone();
}
self.dump().await?;
Ok(())
}
Expand All @@ -446,6 +455,7 @@ impl Updater {
bail!("You are not authenticated.");
}
self.access_token = None;
self.refresh_token = None;
self.dump().await?;
Ok(())
}
Expand All @@ -457,20 +467,40 @@ impl Updater {
return Some(auth);
}
if let Some(token) = &self.access_token {
return Some(AuthInfo::new(token.to_string()));
let mut info = AuthInfo::new(token.to_string());
if let Some(refresh_token) = &self.refresh_token {
info.refresh_token = Some(refresh_token.to_string());
}
return Some(info);
}
None
}

pub fn get_valid_auth(&self) -> Result<AuthInfo> {
pub async fn refresh_auth(&mut self) -> Result<AuthInfo> {
let Some(auth) = self.get_auth() else {
bail!("Not authenticated");
};

let pg = ProgressBar::new_spinner();
pg.set_message("Refreshing auth...");
let refreshed_auth = marzano_auth::auth0::refresh_token(&auth).await?;
self.save_token(&refreshed_auth).await?;

pg.finish_and_clear();
Ok(refreshed_auth)
}

/// Get a valid auth token, refreshing if necessary
pub async fn get_valid_auth(&mut self) -> Result<AuthInfo> {
let auth = self.get_auth();
if let Some(auth) = auth {
if auth.is_expired()? {
bail!("Auth token expired");
}
return Ok(auth);
let Some(auth) = auth else {
bail!("Not authenticated, please run `grit auth login` to authenticate.");
};
if auth.is_expired()? {
let refreshed = self.refresh_auth().await?;
return Ok(refreshed);
}
bail!("Not authenticated");
Ok(auth)
}

async fn download_artifact(&self, app: SupportedApp, artifact_url: String) -> Result<PathBuf> {
Expand Down
6 changes: 3 additions & 3 deletions crates/cli/src/workflows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ where
);
}

let auth = updater.get_valid_auth().map_err(|_| {
let auth = updater.get_valid_auth().await.map_err(|_| {
anyhow::anyhow!(
"No valid authentication token found, please run {}",
style("grit auth login").bold().red()
Expand Down Expand Up @@ -205,7 +205,7 @@ pub async fn run_remote_workflow(
use marzano_gritmodule::fetcher::ModuleRepo;
use std::time::Duration;

let updater = Updater::from_current_bin().await?;
let mut updater = Updater::from_current_bin().await?;
let cwd = std::env::current_dir()?;

let pb = ProgressBar::with_draw_target(Some(0), ProgressDrawTarget::stderr());
Expand All @@ -215,7 +215,7 @@ pub async fn run_remote_workflow(
pb.set_message("Authenticating with Grit Cloud");
pb.enable_steady_tick(Duration::from_millis(60));

let auth = updater.get_valid_auth()?;
let auth = updater.get_valid_auth().await?;

pb.set_message("Launching workflow on Grit Cloud");

Expand Down

0 comments on commit 3681506

Please sign in to comment.