1#![cfg_attr(docsrs, feature(doc_cfg))]
2mod oauth2;
26mod static_provider;
27mod token_endpoint;
28
29use std::sync::Arc;
30use std::time::Duration;
31
32use faucet_core::{FaucetError, SharedAuthProvider};
33use serde_json::Value;
34
35pub(crate) fn auth_http_client() -> reqwest::Client {
42 const AUTH_HTTP_TIMEOUT: Duration = Duration::from_secs(30);
43 reqwest::Client::builder()
44 .timeout(AUTH_HTTP_TIMEOUT)
45 .build()
46 .unwrap_or_else(|_| reqwest::Client::new())
47}
48
49pub use oauth2::{OAuth2ClientCredentialsProvider, OAuth2RefreshProvider};
50pub use static_provider::StaticProvider;
51pub use token_endpoint::TokenEndpointProvider;
52
53pub const DEFAULT_EXPIRY_RATIO: f64 = 0.9;
56
57pub fn build_provider(spec: &Value) -> Result<SharedAuthProvider, FaucetError> {
64 let kind = spec
65 .get("type")
66 .and_then(Value::as_str)
67 .ok_or_else(|| FaucetError::Config("auth provider: missing `type`".into()))?;
68 let config = spec.get("config").cloned().unwrap_or(Value::Null);
69
70 match kind {
71 "static" => Ok(Arc::new(StaticProvider::from_config(&config)?)),
72 "oauth2" => Ok(Arc::new(OAuth2ClientCredentialsProvider::from_config(
73 &config,
74 )?)),
75 "oauth2_refresh" => Ok(Arc::new(OAuth2RefreshProvider::from_config(&config)?)),
76 "token_endpoint" => Ok(Arc::new(TokenEndpointProvider::from_config(&config)?)),
77 other => Err(FaucetError::Config(format!(
78 "auth provider: unknown type `{other}` (expected one of: static, oauth2, oauth2_refresh, token_endpoint)"
79 ))),
80 }
81}
82
83pub(crate) fn expiry_instant(
87 expires_in: Option<u64>,
88 expiry_ratio: f64,
89) -> Option<tokio::time::Instant> {
90 expires_in.map(|secs| {
91 let effective = (secs as f64 * expiry_ratio) as u64;
92 tokio::time::Instant::now() + std::time::Duration::from_secs(effective)
93 })
94}
95
96pub(crate) fn parse_expiry_ratio(config: &Value) -> Result<f64, FaucetError> {
106 match config.get("expiry_ratio") {
107 None | Some(Value::Null) => Ok(DEFAULT_EXPIRY_RATIO),
108 Some(v) => {
109 let r = v.as_f64().ok_or_else(|| {
110 FaucetError::Config(format!(
111 "auth provider: `expiry_ratio` must be a number in (0, 1], got {v}"
112 ))
113 })?;
114 if !r.is_finite() || r <= 0.0 || r > 1.0 {
115 return Err(FaucetError::Config(format!(
116 "auth provider: `expiry_ratio` must be a finite number in (0, 1], got {r}"
117 )));
118 }
119 Ok(r)
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn build_provider_static() {
130 let spec = serde_json::json!({
131 "type": "static",
132 "config": { "token": "abc" }
133 });
134 let p = build_provider(&spec).unwrap();
135 assert_eq!(p.provider_name(), "static");
136 }
137
138 #[test]
139 fn build_provider_unknown_type_errors() {
140 let spec = serde_json::json!({ "type": "magic", "config": {} });
141 let err = build_provider(&spec).unwrap_err();
142 assert!(matches!(err, FaucetError::Config(_)));
143 }
144
145 #[test]
146 fn build_provider_missing_type_errors() {
147 let spec = serde_json::json!({ "config": {} });
148 assert!(build_provider(&spec).is_err());
149 }
150
151 #[test]
152 fn parse_expiry_ratio_validates_range() {
153 use serde_json::json;
154 assert_eq!(
156 parse_expiry_ratio(&json!({})).unwrap(),
157 DEFAULT_EXPIRY_RATIO
158 );
159 assert_eq!(
160 parse_expiry_ratio(&json!({ "expiry_ratio": null })).unwrap(),
161 DEFAULT_EXPIRY_RATIO
162 );
163 assert_eq!(
165 parse_expiry_ratio(&json!({ "expiry_ratio": 0.5 })).unwrap(),
166 0.5
167 );
168 assert_eq!(
169 parse_expiry_ratio(&json!({ "expiry_ratio": 1.0 })).unwrap(),
170 1.0
171 );
172 assert!(parse_expiry_ratio(&json!({ "expiry_ratio": 0 })).is_err());
174 assert!(parse_expiry_ratio(&json!({ "expiry_ratio": -0.5 })).is_err());
175 assert!(parse_expiry_ratio(&json!({ "expiry_ratio": 1.5 })).is_err());
176 assert!(parse_expiry_ratio(&json!({ "expiry_ratio": "0.5" })).is_err());
177 }
178
179 #[test]
180 fn build_provider_rejects_out_of_range_expiry_ratio() {
181 let spec = serde_json::json!({
182 "type": "oauth2",
183 "config": {
184 "token_url": "http://x", "client_id": "id",
185 "client_secret": "sec", "expiry_ratio": 2.0
186 }
187 });
188 assert!(build_provider(&spec).is_err());
189 }
190}