faucet_source_rest/auth/
mod.rs1pub mod api_key;
4pub mod basic;
5pub mod bearer;
6pub mod custom;
7pub mod oauth2;
8pub mod token_endpoint;
9
10use faucet_core::FaucetError;
11use reqwest::header::HeaderMap;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14pub use token_endpoint::ResponseValidator;
15
16#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
18#[serde(tag = "type")]
19pub enum Auth {
20 None,
21 Bearer(String),
22 Basic {
23 username: String,
24 password: String,
25 },
26 ApiKey {
28 header: String,
29 value: String,
30 },
31 ApiKeyQuery {
36 param: String,
37 value: String,
38 },
39 OAuth2 {
40 token_url: String,
41 client_id: String,
42 client_secret: String,
43 scopes: Vec<String>,
44 expiry_ratio: f64,
48 },
49 TokenEndpoint {
58 url: String,
60 #[serde(with = "crate::serde_helpers::http_method")]
62 #[schemars(with = "String")]
63 method: reqwest::Method,
64 #[serde(skip, default)]
66 headers: HeaderMap,
67 body: Option<serde_json::Value>,
69 token_path: String,
71 expiry_path: Option<String>,
74 expiry_ratio: f64,
77 #[serde(skip, default)]
81 response_validator: Option<ResponseValidator>,
82 },
83 #[serde(skip)]
84 Custom(HeaderMap),
85}
86
87impl Auth {
88 pub fn apply(&self, headers: &mut HeaderMap) -> Result<(), FaucetError> {
93 match self {
94 Auth::None | Auth::ApiKeyQuery { .. } => Ok(()),
95 Auth::Bearer(token) => bearer::apply(headers, token),
96 Auth::Basic { username, password } => basic::apply(headers, username, password),
97 Auth::ApiKey { header, value } => api_key::apply(headers, header, value),
98 Auth::OAuth2 { .. } => Err(FaucetError::Auth(
102 "OAuth2 auth must be resolved to a bearer token before applying; \
103 use RestStream (which resolves it automatically) or call \
104 fetch_oauth2_token() and use Auth::Bearer"
105 .into(),
106 )),
107 Auth::TokenEndpoint { .. } => Err(FaucetError::Auth(
109 "TokenEndpoint auth must be resolved to a bearer token before applying; \
110 use RestStream (which resolves it automatically) or call \
111 fetch_token_from_endpoint() and use Auth::Bearer"
112 .into(),
113 )),
114 Auth::Custom(h) => {
115 custom::apply(headers, h);
116 Ok(())
117 }
118 }
119 }
120}
121
122pub use oauth2::fetch_oauth2_token;
123pub use token_endpoint::fetch_token_from_endpoint;
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn auth_none_is_noop() {
131 let mut headers = HeaderMap::new();
132 Auth::None.apply(&mut headers).unwrap();
133 assert!(headers.is_empty());
134 }
135
136 #[test]
137 fn auth_bearer_sets_authorization_header() {
138 let mut headers = HeaderMap::new();
139 Auth::Bearer("my-token".into()).apply(&mut headers).unwrap();
140 assert_eq!(headers.get("authorization").unwrap(), "Bearer my-token");
141 }
142
143 #[test]
144 fn auth_basic_sets_authorization_header() {
145 let mut headers = HeaderMap::new();
146 Auth::Basic {
147 username: "user".into(),
148 password: "pass".into(),
149 }
150 .apply(&mut headers)
151 .unwrap();
152 let value = headers.get("authorization").unwrap().to_str().unwrap();
153 assert!(value.starts_with("Basic "));
154 }
155
156 #[test]
157 fn auth_api_key_sets_custom_header() {
158 let mut headers = HeaderMap::new();
159 Auth::ApiKey {
160 header: "X-Api-Key".into(),
161 value: "secret".into(),
162 }
163 .apply(&mut headers)
164 .unwrap();
165 assert_eq!(headers.get("x-api-key").unwrap(), "secret");
166 }
167
168 #[test]
169 fn auth_api_key_query_is_noop_on_apply() {
170 let mut headers = HeaderMap::new();
171 Auth::ApiKeyQuery {
172 param: "api_key".into(),
173 value: "secret".into(),
174 }
175 .apply(&mut headers)
176 .unwrap();
177 assert!(headers.is_empty());
178 }
179
180 #[test]
181 fn auth_oauth2_errors_on_direct_apply() {
182 let mut headers = HeaderMap::new();
183 let result = Auth::OAuth2 {
184 token_url: "https://auth.example.com/token".into(),
185 client_id: "id".into(),
186 client_secret: "secret".into(),
187 scopes: vec![],
188 expiry_ratio: 0.9,
189 }
190 .apply(&mut headers);
191 assert!(result.is_err());
192 assert!(matches!(result, Err(FaucetError::Auth(_))));
193 }
194
195 #[test]
196 fn auth_token_endpoint_errors_on_direct_apply() {
197 let mut headers = HeaderMap::new();
198 let result = Auth::TokenEndpoint {
199 url: "https://auth.example.com/token".into(),
200 method: reqwest::Method::POST,
201 headers: HeaderMap::new(),
202 body: None,
203 token_path: "$.token".into(),
204 expiry_path: None,
205 expiry_ratio: 0.9,
206 response_validator: None,
207 }
208 .apply(&mut headers);
209 assert!(result.is_err());
210 assert!(matches!(result, Err(FaucetError::Auth(_))));
211 }
212
213 #[test]
214 fn auth_custom_headers() {
215 let mut custom = HeaderMap::new();
216 custom.insert(
217 reqwest::header::HeaderName::from_static("x-custom"),
218 reqwest::header::HeaderValue::from_static("value"),
219 );
220 let mut headers = HeaderMap::new();
221 Auth::Custom(custom).apply(&mut headers).unwrap();
222 assert_eq!(headers.get("x-custom").unwrap(), "value");
223 }
224
225 #[test]
226 fn auth_debug_format() {
227 let auth = Auth::None;
228 assert_eq!(format!("{auth:?}"), "None");
229
230 let auth = Auth::Bearer("tok".into());
231 let debug = format!("{auth:?}");
232 assert!(debug.contains("Bearer"));
233 }
234
235 #[test]
236 fn auth_clone() {
237 let auth = Auth::Bearer("token".into());
238 let cloned = auth.clone();
239 let mut h = HeaderMap::new();
240 cloned.apply(&mut h).unwrap();
241 assert_eq!(h.get("authorization").unwrap(), "Bearer token");
242 }
243}