#![cfg_attr(docsrs, feature(doc_cfg))]
mod oauth2;
mod static_provider;
mod token_endpoint;
use std::sync::Arc;
use std::time::Duration;
use faucet_core::{FaucetError, SharedAuthProvider};
use serde_json::Value;
pub(crate) fn auth_http_client() -> reqwest::Client {
const AUTH_HTTP_TIMEOUT: Duration = Duration::from_secs(30);
reqwest::Client::builder()
.timeout(AUTH_HTTP_TIMEOUT)
.build()
.unwrap_or_else(|_| reqwest::Client::new())
}
pub use oauth2::{OAuth2ClientCredentialsProvider, OAuth2RefreshProvider};
pub use static_provider::StaticProvider;
pub use token_endpoint::TokenEndpointProvider;
pub const DEFAULT_EXPIRY_RATIO: f64 = 0.9;
pub fn build_provider(spec: &Value) -> Result<SharedAuthProvider, FaucetError> {
let kind = spec
.get("type")
.and_then(Value::as_str)
.ok_or_else(|| FaucetError::Config("auth provider: missing `type`".into()))?;
let config = spec.get("config").cloned().unwrap_or(Value::Null);
match kind {
"static" => Ok(Arc::new(StaticProvider::from_config(&config)?)),
"oauth2" => Ok(Arc::new(OAuth2ClientCredentialsProvider::from_config(
&config,
)?)),
"oauth2_refresh" => Ok(Arc::new(OAuth2RefreshProvider::from_config(&config)?)),
"token_endpoint" => Ok(Arc::new(TokenEndpointProvider::from_config(&config)?)),
other => Err(FaucetError::Config(format!(
"auth provider: unknown type `{other}` (expected one of: static, oauth2, oauth2_refresh, token_endpoint)"
))),
}
}
pub(crate) fn expiry_instant(
expires_in: Option<u64>,
expiry_ratio: f64,
) -> Option<tokio::time::Instant> {
expires_in.map(|secs| {
let effective = (secs as f64 * expiry_ratio) as u64;
tokio::time::Instant::now() + std::time::Duration::from_secs(effective)
})
}
pub(crate) fn parse_expiry_ratio(config: &Value) -> Result<f64, FaucetError> {
match config.get("expiry_ratio") {
None | Some(Value::Null) => Ok(DEFAULT_EXPIRY_RATIO),
Some(v) => {
let r = v.as_f64().ok_or_else(|| {
FaucetError::Config(format!(
"auth provider: `expiry_ratio` must be a number in (0, 1], got {v}"
))
})?;
if !r.is_finite() || r <= 0.0 || r > 1.0 {
return Err(FaucetError::Config(format!(
"auth provider: `expiry_ratio` must be a finite number in (0, 1], got {r}"
)));
}
Ok(r)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_provider_static() {
let spec = serde_json::json!({
"type": "static",
"config": { "token": "abc" }
});
let p = build_provider(&spec).unwrap();
assert_eq!(p.provider_name(), "static");
}
#[test]
fn build_provider_unknown_type_errors() {
let spec = serde_json::json!({ "type": "magic", "config": {} });
let err = build_provider(&spec).unwrap_err();
assert!(matches!(err, FaucetError::Config(_)));
}
#[test]
fn build_provider_missing_type_errors() {
let spec = serde_json::json!({ "config": {} });
assert!(build_provider(&spec).is_err());
}
#[test]
fn parse_expiry_ratio_validates_range() {
use serde_json::json;
assert_eq!(
parse_expiry_ratio(&json!({})).unwrap(),
DEFAULT_EXPIRY_RATIO
);
assert_eq!(
parse_expiry_ratio(&json!({ "expiry_ratio": null })).unwrap(),
DEFAULT_EXPIRY_RATIO
);
assert_eq!(
parse_expiry_ratio(&json!({ "expiry_ratio": 0.5 })).unwrap(),
0.5
);
assert_eq!(
parse_expiry_ratio(&json!({ "expiry_ratio": 1.0 })).unwrap(),
1.0
);
assert!(parse_expiry_ratio(&json!({ "expiry_ratio": 0 })).is_err());
assert!(parse_expiry_ratio(&json!({ "expiry_ratio": -0.5 })).is_err());
assert!(parse_expiry_ratio(&json!({ "expiry_ratio": 1.5 })).is_err());
assert!(parse_expiry_ratio(&json!({ "expiry_ratio": "0.5" })).is_err());
}
#[test]
fn build_provider_rejects_out_of_range_expiry_ratio() {
let spec = serde_json::json!({
"type": "oauth2",
"config": {
"token_url": "http://x", "client_id": "id",
"client_secret": "sec", "expiry_ratio": 2.0
}
});
assert!(build_provider(&spec).is_err());
}
}