291 lines
10 KiB
Rust
291 lines
10 KiB
Rust
use curl::easy::Easy;
|
|
use std::collections::HashMap;
|
|
|
|
use crate::{
|
|
error::{RelayError, Result},
|
|
interop::{ApiKeyLocation, AuthType, GrantType, TokenResponse},
|
|
};
|
|
|
|
pub(crate) struct AuthHandler<'a> {
|
|
handle: &'a mut Easy,
|
|
headers: &'a mut HashMap<String, String>,
|
|
}
|
|
|
|
impl<'a> AuthHandler<'a> {
|
|
pub(crate) fn new(handle: &'a mut Easy, headers: &'a mut HashMap<String, String>) -> Self {
|
|
Self { handle, headers }
|
|
}
|
|
|
|
#[tracing::instrument(skip(self), level = "debug")]
|
|
pub(crate) fn set_auth(&mut self, auth: &AuthType) -> Result<()> {
|
|
match auth {
|
|
AuthType::Basic { username, password } => {
|
|
tracing::info!(username = %username, "Setting basic auth");
|
|
self.set_basic_auth(username, password)
|
|
}
|
|
AuthType::Bearer { token } => {
|
|
tracing::info!("Setting bearer auth");
|
|
self.set_bearer_auth(token)
|
|
}
|
|
AuthType::Digest {
|
|
username, password, ..
|
|
} => {
|
|
tracing::info!(username = %username, "Setting digest auth");
|
|
self.set_digest_auth(username, password)
|
|
}
|
|
AuthType::ApiKey { key, value, location } => {
|
|
tracing::info!(key = %key, "Setting API key auth");
|
|
self.set_apikey_auth(key, value, location)
|
|
}
|
|
AuthType::OAuth2 {
|
|
grant_type,
|
|
access_token,
|
|
refresh_token,
|
|
} => {
|
|
if let Some(token) = access_token {
|
|
tracing::info!("Using existing OAuth2 access token");
|
|
self.set_bearer_auth(token)
|
|
} else if let Some(refresh_token) = refresh_token {
|
|
tracing::info!("Refreshing OAuth2 token");
|
|
self.refresh_oauth2_token(grant_type, refresh_token)
|
|
} else {
|
|
tracing::info!("Initiating OAuth2 flow");
|
|
self.handle_oauth2_flow(grant_type)
|
|
}
|
|
}
|
|
AuthType::None => {
|
|
tracing::info!("No authentication required");
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_basic_auth(&mut self, username: &str, password: &str) -> Result<()> {
|
|
tracing::debug!(username = %username, "Setting basic auth credentials");
|
|
|
|
self.handle.username(username).map_err(|e| {
|
|
tracing::error!(error = %e, "Failed to set username");
|
|
RelayError::Network {
|
|
message: "Failed to set username".into(),
|
|
cause: Some(e.to_string()),
|
|
}
|
|
})?;
|
|
|
|
self.handle.password(password).map_err(|e| {
|
|
tracing::error!(error = %e, "Failed to set password");
|
|
RelayError::Network {
|
|
message: "Failed to set password".into(),
|
|
cause: Some(e.to_string()),
|
|
}
|
|
})?;
|
|
|
|
tracing::debug!("Basic auth credentials set successfully");
|
|
Ok(())
|
|
}
|
|
|
|
fn set_bearer_auth(&mut self, token: &str) -> Result<()> {
|
|
self.headers
|
|
.insert("Authorization".to_string(), format!("Bearer {}", token));
|
|
Ok(())
|
|
}
|
|
|
|
fn set_apikey_auth(&mut self, key: &str, value: &str, location: &ApiKeyLocation) -> Result<()> {
|
|
tracing::debug!(key = %key, location = ?location, "Setting API key auth");
|
|
|
|
match location {
|
|
ApiKeyLocation::Header => {
|
|
tracing::debug!("Adding API key as header: {}", key);
|
|
self.headers.insert(key.to_string(), value.to_string());
|
|
}
|
|
ApiKeyLocation::Query => {
|
|
// For query parameters, we don't need to do anything here
|
|
// This is handled in the request.rs file before setting the URL
|
|
tracing::debug!("API key will be added to query parameters in URL");
|
|
}
|
|
}
|
|
|
|
tracing::debug!("API key auth configured successfully");
|
|
Ok(())
|
|
}
|
|
|
|
fn set_digest_auth(&mut self, username: &str, password: &str) -> Result<()> {
|
|
tracing::debug!("Setting up digest authentication");
|
|
self.set_basic_auth(username, password)?;
|
|
|
|
let mut auth = curl::easy::Auth::new();
|
|
auth.digest(true);
|
|
|
|
tracing::info!("Configuring digest auth mode");
|
|
self.handle.http_auth(&auth).map_err(|e| {
|
|
tracing::error!(error = %e, "Failed to set digest authentication");
|
|
RelayError::Network {
|
|
message: "Failed to set digest authentication".into(),
|
|
cause: Some(e.to_string()),
|
|
}
|
|
})?;
|
|
|
|
tracing::debug!("Digest auth configured successfully");
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_oauth2_flow(&mut self, grant_type: &GrantType) -> Result<()> {
|
|
match grant_type {
|
|
GrantType::ClientCredentials {
|
|
token_endpoint,
|
|
client_id,
|
|
client_secret,
|
|
} => {
|
|
tracing::info!("Initiating client credentials flow");
|
|
self.client_credentials_flow(token_endpoint, client_id, client_secret.as_deref())
|
|
}
|
|
GrantType::Password {
|
|
token_endpoint,
|
|
username,
|
|
password,
|
|
} => {
|
|
tracing::info!("Initiating password flow");
|
|
self.password_flow(token_endpoint, username, password)
|
|
}
|
|
GrantType::AuthorizationCode { .. } => {
|
|
tracing::warn!("Authorization Code flow not supported");
|
|
Err(RelayError::UnsupportedFeature {
|
|
feature: "Authorization Code Grant".into(),
|
|
message: "Authorization Code flow requires browser interaction".into(),
|
|
relay: "curl".into(),
|
|
})
|
|
}
|
|
GrantType::Implicit { .. } => {
|
|
tracing::warn!("Implicit flow not supported");
|
|
Err(RelayError::UnsupportedFeature {
|
|
feature: "Implicit Grant".into(),
|
|
message: "Implicit flow requires browser interaction".into(),
|
|
relay: "curl".into(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn refresh_oauth2_token(&mut self, grant_type: &GrantType, refresh_token: &str) -> Result<()> {
|
|
let token_endpoint = match grant_type {
|
|
GrantType::ClientCredentials { token_endpoint, .. }
|
|
| GrantType::Password { token_endpoint, .. }
|
|
| GrantType::AuthorizationCode { token_endpoint, .. } => token_endpoint,
|
|
GrantType::Implicit { .. } => {
|
|
tracing::error!("Attempted to refresh token with implicit grant");
|
|
return Err(RelayError::UnsupportedFeature {
|
|
feature: "Token Refresh".into(),
|
|
message: "Implicit grant does not support refresh tokens".into(),
|
|
relay: "curl".into(),
|
|
});
|
|
}
|
|
};
|
|
|
|
let params = vec![
|
|
("grant_type", "refresh_token"),
|
|
("refresh_token", refresh_token),
|
|
];
|
|
|
|
self.request_token(token_endpoint, ¶ms)
|
|
}
|
|
|
|
fn client_credentials_flow(
|
|
&mut self,
|
|
token_endpoint: &str,
|
|
client_id: &str,
|
|
client_secret: Option<&str>,
|
|
) -> Result<()> {
|
|
tracing::info!("Performing client credentials flow");
|
|
|
|
let mut params = vec![
|
|
("grant_type", "client_credentials"),
|
|
("client_id", client_id),
|
|
];
|
|
|
|
if let Some(secret) = client_secret {
|
|
params.push(("client_secret", secret));
|
|
}
|
|
|
|
self.request_token(token_endpoint, ¶ms)
|
|
}
|
|
|
|
fn password_flow(
|
|
&mut self,
|
|
token_endpoint: &str,
|
|
username: &str,
|
|
password: &str,
|
|
) -> Result<()> {
|
|
tracing::info!("Performing password flow");
|
|
|
|
let params = vec![
|
|
("grant_type", "password"),
|
|
("username", username),
|
|
("password", password),
|
|
];
|
|
|
|
self.request_token(token_endpoint, ¶ms)
|
|
}
|
|
|
|
fn request_token(&mut self, token_endpoint: &str, params: &[(&str, &str)]) -> Result<()> {
|
|
let mut handle = Easy::new();
|
|
tracing::debug!(endpoint = %token_endpoint, "Requesting OAuth2 token");
|
|
|
|
handle.url(token_endpoint).map_err(|e| {
|
|
tracing::error!(error = %e, "Failed to set token endpoint URL");
|
|
RelayError::Network {
|
|
message: "Failed to set token endpoint URL".into(),
|
|
cause: Some(e.to_string()),
|
|
}
|
|
})?;
|
|
|
|
let form_data: String = params
|
|
.iter()
|
|
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
|
|
.collect::<Vec<_>>()
|
|
.join("&");
|
|
|
|
handle.post_fields_copy(form_data.as_bytes()).map_err(|e| {
|
|
tracing::error!(error = %e, "Failed to set form data");
|
|
RelayError::Network {
|
|
message: "Failed to set form data".into(),
|
|
cause: Some(e.to_string()),
|
|
}
|
|
})?;
|
|
|
|
let mut response = Vec::new();
|
|
{
|
|
let mut transfer = handle.transfer();
|
|
transfer
|
|
.write_function(|data| {
|
|
response.extend_from_slice(data);
|
|
Ok(data.len())
|
|
})
|
|
.map_err(|e| {
|
|
tracing::error!(error = %e, "Failed to set write callback");
|
|
RelayError::Network {
|
|
message: "Failed to set write callback".into(),
|
|
cause: Some(e.to_string()),
|
|
}
|
|
})?;
|
|
|
|
tracing::debug!("Performing token request");
|
|
transfer.perform().map_err(|e| {
|
|
tracing::error!(error = %e, "Failed to perform token request");
|
|
RelayError::Network {
|
|
message: "Failed to perform token request".into(),
|
|
cause: Some(e.to_string()),
|
|
}
|
|
})?;
|
|
}
|
|
|
|
let token_response: TokenResponse = serde_json::from_slice(&response).map_err(|e| {
|
|
tracing::error!(error = %e, "Failed to parse token response");
|
|
RelayError::Parse {
|
|
message: "Failed to parse token response".into(),
|
|
cause: Some(e.to_string()),
|
|
}
|
|
})?;
|
|
|
|
tracing::info!("Successfully obtained OAuth2 token");
|
|
self.set_bearer_auth(&token_response.access_token)
|
|
}
|
|
}
|