pub mod api_key;
pub mod basic;
pub mod bearer;
pub mod custom;
pub mod oauth2;
pub mod token_endpoint;
use faucet_core::FaucetError;
use reqwest::header::HeaderMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub use token_endpoint::ResponseValidator;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
pub enum Auth {
None,
Bearer(String),
Basic {
username: String,
password: String,
},
ApiKey {
header: String,
value: String,
},
ApiKeyQuery {
param: String,
value: String,
},
OAuth2 {
token_url: String,
client_id: String,
client_secret: String,
scopes: Vec<String>,
expiry_ratio: f64,
},
TokenEndpoint {
url: String,
#[serde(with = "crate::serde_helpers::http_method")]
#[schemars(with = "String")]
method: reqwest::Method,
#[serde(skip, default)]
headers: HeaderMap,
body: Option<serde_json::Value>,
token_path: String,
expiry_path: Option<String>,
expiry_ratio: f64,
#[serde(skip, default)]
response_validator: Option<ResponseValidator>,
},
#[serde(skip)]
Custom(HeaderMap),
}
impl Auth {
pub fn apply(&self, headers: &mut HeaderMap) -> Result<(), FaucetError> {
match self {
Auth::None | Auth::ApiKeyQuery { .. } => Ok(()),
Auth::Bearer(token) => bearer::apply(headers, token),
Auth::Basic { username, password } => basic::apply(headers, username, password),
Auth::ApiKey { header, value } => api_key::apply(headers, header, value),
Auth::OAuth2 { .. } => Err(FaucetError::Auth(
"OAuth2 auth must be resolved to a bearer token before applying; \
use RestStream (which resolves it automatically) or call \
fetch_oauth2_token() and use Auth::Bearer"
.into(),
)),
Auth::TokenEndpoint { .. } => Err(FaucetError::Auth(
"TokenEndpoint auth must be resolved to a bearer token before applying; \
use RestStream (which resolves it automatically) or call \
fetch_token_from_endpoint() and use Auth::Bearer"
.into(),
)),
Auth::Custom(h) => {
custom::apply(headers, h);
Ok(())
}
}
}
}
pub use oauth2::fetch_oauth2_token;
pub use token_endpoint::fetch_token_from_endpoint;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_none_is_noop() {
let mut headers = HeaderMap::new();
Auth::None.apply(&mut headers).unwrap();
assert!(headers.is_empty());
}
#[test]
fn auth_bearer_sets_authorization_header() {
let mut headers = HeaderMap::new();
Auth::Bearer("my-token".into()).apply(&mut headers).unwrap();
assert_eq!(headers.get("authorization").unwrap(), "Bearer my-token");
}
#[test]
fn auth_basic_sets_authorization_header() {
let mut headers = HeaderMap::new();
Auth::Basic {
username: "user".into(),
password: "pass".into(),
}
.apply(&mut headers)
.unwrap();
let value = headers.get("authorization").unwrap().to_str().unwrap();
assert!(value.starts_with("Basic "));
}
#[test]
fn auth_api_key_sets_custom_header() {
let mut headers = HeaderMap::new();
Auth::ApiKey {
header: "X-Api-Key".into(),
value: "secret".into(),
}
.apply(&mut headers)
.unwrap();
assert_eq!(headers.get("x-api-key").unwrap(), "secret");
}
#[test]
fn auth_api_key_query_is_noop_on_apply() {
let mut headers = HeaderMap::new();
Auth::ApiKeyQuery {
param: "api_key".into(),
value: "secret".into(),
}
.apply(&mut headers)
.unwrap();
assert!(headers.is_empty());
}
#[test]
fn auth_oauth2_errors_on_direct_apply() {
let mut headers = HeaderMap::new();
let result = Auth::OAuth2 {
token_url: "https://auth.example.com/token".into(),
client_id: "id".into(),
client_secret: "secret".into(),
scopes: vec![],
expiry_ratio: 0.9,
}
.apply(&mut headers);
assert!(result.is_err());
assert!(matches!(result, Err(FaucetError::Auth(_))));
}
#[test]
fn auth_token_endpoint_errors_on_direct_apply() {
let mut headers = HeaderMap::new();
let result = Auth::TokenEndpoint {
url: "https://auth.example.com/token".into(),
method: reqwest::Method::POST,
headers: HeaderMap::new(),
body: None,
token_path: "$.token".into(),
expiry_path: None,
expiry_ratio: 0.9,
response_validator: None,
}
.apply(&mut headers);
assert!(result.is_err());
assert!(matches!(result, Err(FaucetError::Auth(_))));
}
#[test]
fn auth_custom_headers() {
let mut custom = HeaderMap::new();
custom.insert(
reqwest::header::HeaderName::from_static("x-custom"),
reqwest::header::HeaderValue::from_static("value"),
);
let mut headers = HeaderMap::new();
Auth::Custom(custom).apply(&mut headers).unwrap();
assert_eq!(headers.get("x-custom").unwrap(), "value");
}
#[test]
fn auth_debug_format() {
let auth = Auth::None;
assert_eq!(format!("{auth:?}"), "None");
let auth = Auth::Bearer("tok".into());
let debug = format!("{auth:?}");
assert!(debug.contains("Bearer"));
}
#[test]
fn auth_clone() {
let auth = Auth::Bearer("token".into());
let cloned = auth.clone();
let mut h = HeaderMap::new();
cloned.apply(&mut h).unwrap();
assert_eq!(h.get("authorization").unwrap(), "Bearer token");
}
}