1#![cfg_attr(docsrs, feature(doc_cfg))]
2mod flow;
26#[cfg(feature = "oauth1")]
27mod oauth1;
28mod oauth2;
29mod static_provider;
30mod token_endpoint;
31
32use std::sync::Arc;
33use std::time::Duration;
34
35use faucet_core::{FaucetError, SharedAuthProvider};
36use serde_json::Value;
37
38pub(crate) fn auth_http_client() -> reqwest::Client {
45 const AUTH_HTTP_TIMEOUT: Duration = Duration::from_secs(30);
46 reqwest::Client::builder()
47 .timeout(AUTH_HTTP_TIMEOUT)
48 .build()
49 .unwrap_or_else(|_| reqwest::Client::new())
50}
51
52pub use flow::FlowProvider;
53#[cfg(feature = "oauth1")]
54pub use oauth1::OAuth1Provider;
55pub use oauth2::{OAuth2ClientCredentialsProvider, OAuth2RefreshProvider};
56pub use static_provider::StaticProvider;
57pub use token_endpoint::TokenEndpointProvider;
58
59pub const DEFAULT_EXPIRY_RATIO: f64 = 0.9;
62
63pub fn build_provider(spec: &Value) -> Result<SharedAuthProvider, FaucetError> {
70 let kind = spec
71 .get("type")
72 .and_then(Value::as_str)
73 .ok_or_else(|| FaucetError::Config("auth provider: missing `type`".into()))?;
74 let config = spec.get("config").cloned().unwrap_or(Value::Null);
75
76 match kind {
77 "flow" => Ok(Arc::new(FlowProvider::from_config(&config)?)),
78 "static" => Ok(Arc::new(StaticProvider::from_config(&config)?)),
79 "oauth2" => Ok(Arc::new(OAuth2ClientCredentialsProvider::from_config(
80 &config,
81 )?)),
82 "oauth2_refresh" => Ok(Arc::new(OAuth2RefreshProvider::from_config(&config)?)),
83 "token_endpoint" => Ok(Arc::new(TokenEndpointProvider::from_config(&config)?)),
84 "oauth1" => {
85 #[cfg(feature = "oauth1")]
86 {
87 Ok(Arc::new(OAuth1Provider::from_config(&config)?))
88 }
89 #[cfg(not(feature = "oauth1"))]
90 {
91 Err(FaucetError::Config(
92 "auth provider: `oauth1` requires the `oauth1` feature — rebuild with \
93 `--features oauth1` (e.g. `cargo install faucet-cli --features oauth1`)"
94 .into(),
95 ))
96 }
97 }
98 other => Err(FaucetError::Config(format!(
99 "auth provider: unknown type `{other}` (expected one of: flow, static, oauth2, oauth2_refresh, token_endpoint, oauth1)"
100 ))),
101 }
102}
103
104pub(crate) fn expiry_instant(
108 expires_in: Option<u64>,
109 expiry_ratio: f64,
110) -> Option<tokio::time::Instant> {
111 expires_in.map(|secs| {
112 let effective = (secs as f64 * expiry_ratio) as u64;
113 tokio::time::Instant::now() + std::time::Duration::from_secs(effective)
114 })
115}
116
117pub(crate) fn parse_expiry_ratio(config: &Value) -> Result<f64, FaucetError> {
127 match config.get("expiry_ratio") {
128 None | Some(Value::Null) => Ok(DEFAULT_EXPIRY_RATIO),
129 Some(v) => {
130 let r = v.as_f64().ok_or_else(|| {
131 FaucetError::Config(format!(
132 "auth provider: `expiry_ratio` must be a number in (0, 1], got {v}"
133 ))
134 })?;
135 if !r.is_finite() || r <= 0.0 || r > 1.0 {
136 return Err(FaucetError::Config(format!(
137 "auth provider: `expiry_ratio` must be a finite number in (0, 1], got {r}"
138 )));
139 }
140 Ok(r)
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn build_provider_static() {
151 let spec = serde_json::json!({
152 "type": "static",
153 "config": { "token": "abc" }
154 });
155 let p = build_provider(&spec).unwrap();
156 assert_eq!(p.provider_name(), "static");
157 }
158
159 #[test]
160 fn build_provider_unknown_type_errors() {
161 let spec = serde_json::json!({ "type": "magic", "config": {} });
162 let err = build_provider(&spec).unwrap_err();
163 assert!(matches!(err, FaucetError::Config(_)));
164 }
165
166 #[test]
167 fn build_provider_missing_type_errors() {
168 let spec = serde_json::json!({ "config": {} });
169 assert!(build_provider(&spec).is_err());
170 }
171
172 #[test]
173 fn parse_expiry_ratio_validates_range() {
174 use serde_json::json;
175 assert_eq!(
177 parse_expiry_ratio(&json!({})).unwrap(),
178 DEFAULT_EXPIRY_RATIO
179 );
180 assert_eq!(
181 parse_expiry_ratio(&json!({ "expiry_ratio": null })).unwrap(),
182 DEFAULT_EXPIRY_RATIO
183 );
184 assert_eq!(
186 parse_expiry_ratio(&json!({ "expiry_ratio": 0.5 })).unwrap(),
187 0.5
188 );
189 assert_eq!(
190 parse_expiry_ratio(&json!({ "expiry_ratio": 1.0 })).unwrap(),
191 1.0
192 );
193 assert!(parse_expiry_ratio(&json!({ "expiry_ratio": 0 })).is_err());
195 assert!(parse_expiry_ratio(&json!({ "expiry_ratio": -0.5 })).is_err());
196 assert!(parse_expiry_ratio(&json!({ "expiry_ratio": 1.5 })).is_err());
197 assert!(parse_expiry_ratio(&json!({ "expiry_ratio": "0.5" })).is_err());
198 }
199
200 #[test]
201 fn build_provider_rejects_out_of_range_expiry_ratio() {
202 let spec = serde_json::json!({
203 "type": "oauth2",
204 "config": {
205 "token_url": "http://x", "client_id": "id",
206 "client_secret": "sec", "expiry_ratio": 2.0
207 }
208 });
209 assert!(build_provider(&spec).is_err());
210 }
211}