diff --git a/src/api/client.rs b/src/api/client.rs index 881680a..f11ff89 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -70,9 +70,23 @@ impl ApiClient { }; match auth_type.as_deref() { + Some("app") => { + if let Some(token) = auth.borrow().bearer_token() { + Ok(format!("Bearer {}", token)) + } else { + Err(Error::AuthError(AuthError::WrongTokenFoundInStore)) + } + } Some("oauth2") => self.get_oauth2_token(auth, username).await, Some("oauth1") => Ok(auth.borrow().oauth1(method, url, None)?), None => { + // if no auth type is provided, we are using the first oauth2 token, if it exists + // if no oauth2 token is found, we are using the saved oauth1 tokens, if they exist + // if no oauth1 tokens are found, we start the oauth2 pkce flow + // TODO: we need to have a store of routes that are protected by oauth2 and oauth1 + // depending on the route, we will prioritize the auth type and use the correct token + // this will allow the user to not have to specify the auth type for each request and + // xurl will be able to choose the correct auth type based on the route let token = { let auth_ref = auth.borrow(); auth_ref.first_oauth2_token() @@ -218,6 +232,13 @@ mod tests { auth } + fn setup_tests_with_mock_app_auth() -> Auth { + let mut auth = mock_auth(); + let token_store = auth.get_token_store(); + token_store.save_bearer_token("fake_token").unwrap(); + auth + } + fn cleanup_token_store() { let mut auth = mock_auth(); let token_store = auth.get_token_store(); @@ -277,6 +298,31 @@ mod tests { cleanup_token_store(); } + #[tokio::test] + async fn test_successful_get_request_app_auth() { + setup_env(); + let mut server = Server::new_async().await; + let url = server.url(); + let mock = server + .mock("GET", "/2/users/me") + .with_status(200) + .with_body(r#"{"data":{"id":"123","name":"test"}}"#) + .create_async() + .await; + + let config = Config::from_env().unwrap(); + let client = ApiClient::new(config) + .with_url(url) + .with_auth(setup_tests_with_mock_app_auth()); + let result = client + .send_request("GET", "/2/users/me", &[], None, Some("app"), None) + .await; + + assert!(result.is_ok()); + mock.assert_async().await; + cleanup_token_store(); + } + #[tokio::test] async fn test_error_response() { setup_env(); diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 97f81b6..26f10e5 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -4,6 +4,7 @@ pub mod token_store; use crate::auth::listener::listen_for_code; use crate::auth::token_store::Token; use crate::auth::token_store::TokenStore; +use crate::auth::token_store::TokenStoreError; use crate::config::Config; use oauth2::basic::BasicClient; @@ -29,8 +30,6 @@ pub enum AuthError { InvalidCode(String), #[error("Invalid token: {0}")] InvalidToken(String), - #[error("Token store error: {0}")] - TokenStoreError(String), #[error("Authorization error: {0}")] AuthorizationError(String), #[error("Network error: {0}")] @@ -43,6 +42,8 @@ pub enum AuthError { InvalidAuthType(String), #[error("Non-OAuth2 tokens found when looking for OAuth2 token")] WrongTokenFoundInStore, + #[error("Token store error: {0}")] + TokenStoreError(#[from] TokenStoreError), } pub struct Auth { @@ -199,13 +200,21 @@ impl Auth { .ok_or_else(|| AuthError::NetworkError("Missing username field".to_string()))? .to_string(); - self.token_store - .save_oauth2_token(&username, &token) - .map_err(|e| AuthError::TokenStoreError(e.to_string()))?; + self.token_store.save_oauth2_token(&username, &token)?; Ok(token) } + pub fn bearer_token(&self) -> Option { + self.token_store + .get_bearer_token() + .as_ref() + .and_then(|token| match token { + Token::Bearer(token) => Some(token.clone()), + _ => None, + }) + } + pub fn first_oauth2_token(&self) -> Option { self.token_store.get_first_oauth2_token() } diff --git a/src/auth/token_store.rs b/src/auth/token_store.rs index 4f0a25d..7f7592f 100644 --- a/src/auth/token_store.rs +++ b/src/auth/token_store.rs @@ -1,4 +1,3 @@ -use crate::error::Error; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; @@ -22,6 +21,16 @@ pub enum Token { OAuth1(OAuth1Token), } +#[derive(thiserror::Error, Debug, Serialize, Deserialize)] +pub enum TokenStoreError { + #[error("JSON serialization error")] + JSONSerializationError, + #[error("JSON deserialization error")] + JSONDeserializationError, + #[error("IO error")] + IOError, +} + #[derive(Debug, Serialize, Deserialize)] pub struct TokenStore { oauth2_tokens: HashMap, // username -> access_token @@ -60,7 +69,16 @@ impl TokenStore { store } - pub fn save_oauth2_token(&mut self, username: &str, token: &str) -> Result<(), Error> { + pub fn save_bearer_token(&mut self, token: &str) -> Result<(), TokenStoreError> { + self.bearer_token = Some(Token::Bearer(token.to_string())); + self.save_to_file() + } + + pub fn save_oauth2_token( + &mut self, + username: &str, + token: &str, + ) -> Result<(), TokenStoreError> { self.oauth2_tokens .insert(username.to_string(), Token::OAuth2(token.to_string())); self.save_to_file() @@ -72,7 +90,7 @@ impl TokenStore { token_secret: String, consumer_key: String, consumer_secret: String, - ) -> Result<(), Error> { + ) -> Result<(), TokenStoreError> { self.oauth1_tokens = Some(Token::OAuth1(OAuth1Token { access_token, token_secret, @@ -94,19 +112,29 @@ impl TokenStore { self.oauth1_tokens.clone() } - pub fn clear_oauth2_token(&mut self, username: &str) -> Result<(), Error> { + pub fn get_bearer_token(&self) -> Option { + self.bearer_token.clone() + } + + pub fn clear_oauth2_token(&mut self, username: &str) -> Result<(), TokenStoreError> { self.oauth2_tokens.remove(username); self.save_to_file() } - pub fn clear_oauth1_tokens(&mut self) -> Result<(), Error> { + pub fn clear_oauth1_tokens(&mut self) -> Result<(), TokenStoreError> { self.oauth1_tokens = None; self.save_to_file() } - pub fn clear_all(&mut self) -> Result<(), Error> { + pub fn clear_all(&mut self) -> Result<(), TokenStoreError> { self.oauth2_tokens.clear(); self.oauth1_tokens = None; + self.bearer_token = None; + self.save_to_file() + } + + pub fn clear_bearer_token(&mut self) -> Result<(), TokenStoreError> { + self.bearer_token = None; self.save_to_file() } @@ -118,9 +146,10 @@ impl TokenStore { self.oauth1_tokens.is_some() } - fn save_to_file(&self) -> Result<(), Error> { - let content = serde_json::to_string(&self)?; - fs::write(&self.file_path, content)?; + fn save_to_file(&self) -> Result<(), TokenStoreError> { + let content = + serde_json::to_string(&self).map_err(|_| TokenStoreError::JSONSerializationError)?; + fs::write(&self.file_path, content).map_err(|_| TokenStoreError::IOError)?; Ok(()) } } diff --git a/src/cli.rs b/src/cli.rs index 67d8b6b..b0785a7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -49,6 +49,13 @@ pub enum Commands { #[derive(Subcommand)] pub enum AuthCommands { + /// Configure app-auth + #[command(name = "app")] + App { + #[arg(long)] + bearer_token: String, + }, + /// Configure OAuth2 authentication #[command(name = "oauth2")] OAuth2, @@ -77,5 +84,7 @@ pub enum AuthCommands { oauth1: bool, #[arg(long)] oauth2_username: Option, + #[arg(long)] + bearer: bool, }, } diff --git a/src/main.rs b/src/main.rs index c17b448..31157dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,13 @@ async fn main() -> Result<(), Error> { // Handle auth subcommands if let Some(Commands::Auth { command }) = cli.command { match command { + AuthCommands::App { bearer_token } => { + auth.get_token_store() + .save_bearer_token(&bearer_token) + .map_err(|e| Error::AuthError(auth::AuthError::TokenStoreError(e)))?; + println!("App authentication successful!"); + } + AuthCommands::OAuth2 => { auth.oauth2(None).await?; println!("OAuth2 authentication successful!"); @@ -32,13 +39,9 @@ async fn main() -> Result<(), Error> { access_token, token_secret, } => { - let mut store = TokenStore::new(); - store.save_oauth1_tokens( - access_token, - token_secret, - consumer_key, - consumer_secret, - )?; + auth.get_token_store() + .save_oauth1_tokens(access_token, token_secret, consumer_key, consumer_secret) + .map_err(|e| Error::AuthError(auth::AuthError::TokenStoreError(e)))?; println!("OAuth1 credentials saved successfully!"); } @@ -62,17 +65,28 @@ async fn main() -> Result<(), Error> { all, oauth1, oauth2_username, + bearer, } => { - let mut store = TokenStore::new(); if all { - store.clear_all()?; + auth.get_token_store() + .clear_all() + .map_err(|e| Error::AuthError(auth::AuthError::TokenStoreError(e)))?; println!("All authentication cleared!"); } else if oauth1 { - store.clear_oauth1_tokens()?; + auth.get_token_store() + .clear_oauth1_tokens() + .map_err(|e| Error::AuthError(auth::AuthError::TokenStoreError(e)))?; println!("OAuth1 tokens cleared!"); } else if let Some(username) = oauth2_username { - store.clear_oauth2_token(&username)?; + auth.get_token_store() + .clear_oauth2_token(&username) + .map_err(|e| Error::AuthError(auth::AuthError::TokenStoreError(e)))?; println!("OAuth2 token cleared for {}!", username); + } else if bearer { + auth.get_token_store() + .clear_bearer_token() + .map_err(|e| Error::AuthError(auth::AuthError::TokenStoreError(e)))?; + println!("Bearer token cleared!"); } else { println!("No authentication cleared! Use --all to clear all authentication."); std::process::exit(1);