Skip to content

Commit

Permalink
Add app authentication support with bearer token management and updat…
Browse files Browse the repository at this point in the history
…e various related functionalities.
  • Loading branch information
Santiago Medina Rolong committed Dec 17, 2024
1 parent b9b1431 commit 762b277
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 25 deletions.
46 changes: 46 additions & 0 deletions src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
19 changes: 14 additions & 5 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}")]
Expand All @@ -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 {
Expand Down Expand Up @@ -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<String> {
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<Token> {
self.token_store.get_first_oauth2_token()
}
Expand Down
47 changes: 38 additions & 9 deletions src/auth/token_store.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::error::Error;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
Expand All @@ -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<String, Token>, // username -> access_token
Expand Down Expand Up @@ -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()
Expand All @@ -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,
Expand All @@ -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<Token> {
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()
}

Expand All @@ -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(())
}
}
9 changes: 9 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -77,5 +84,7 @@ pub enum AuthCommands {
oauth1: bool,
#[arg(long)]
oauth2_username: Option<String>,
#[arg(long)]
bearer: bool,
},
}
36 changes: 25 additions & 11 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!");
Expand All @@ -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!");
}

Expand All @@ -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);
Expand Down

0 comments on commit 762b277

Please sign in to comment.