1use crate::error::{Error, Result};
4use url::Url;
5
6#[cfg(feature = "api-key")]
7use api_keys_simplified::{
8 ApiKey, ApiKeyManagerV0, Environment, ExposeSecret, HashConfig, KeyConfig, KeyStatus,
9 SecureString,
10};
11
12use super::types::{GeneratedApiKey, OAuthProvider};
13
14#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
16pub struct OAuthConfig {
17 #[serde(default)]
19 pub enabled: bool,
20 #[serde(default)]
22 pub client_id: Option<String>,
23 #[serde(default)]
25 pub client_secret: Option<String>,
26 #[serde(default)]
28 pub redirect_uri: Option<String>,
29 #[serde(default)]
31 pub authorization_endpoint: Option<String>,
32 #[serde(default)]
34 pub token_endpoint: Option<String>,
35 #[serde(default = "default_oauth_scopes")]
37 pub scopes: Vec<String>,
38 #[serde(default)]
40 pub provider: OAuthProvider,
41}
42
43fn default_oauth_scopes() -> Vec<String> {
44 vec![
45 "openid".to_string(),
46 "profile".to_string(),
47 "email".to_string(),
48 ]
49}
50
51impl Default for OAuthConfig {
52 fn default() -> Self {
53 Self {
54 enabled: false,
55 client_id: None,
56 client_secret: None,
57 redirect_uri: None,
58 authorization_endpoint: None,
59 token_endpoint: None,
60 scopes: vec![
61 "openid".to_string(),
62 "profile".to_string(),
63 "email".to_string(),
64 ],
65 provider: OAuthProvider::Custom,
66 }
67 }
68}
69
70impl OAuthConfig {
71 #[must_use]
73 pub fn github(client_id: String, client_secret: String, redirect_uri: String) -> Self {
74 Self {
75 enabled: true,
76 client_id: Some(client_id),
77 client_secret: Some(client_secret),
78 redirect_uri: Some(redirect_uri),
79 authorization_endpoint: Some("https://github.com/login/oauth/authorize".to_string()),
80 token_endpoint: Some("https://github.com/login/oauth/access_token".to_string()),
81 scopes: vec!["read:user".to_string(), "user:email".to_string()],
82 provider: OAuthProvider::GitHub,
83 }
84 }
85
86 #[must_use]
88 pub fn google(client_id: String, client_secret: String, redirect_uri: String) -> Self {
89 Self {
90 enabled: true,
91 client_id: Some(client_id),
92 client_secret: Some(client_secret),
93 redirect_uri: Some(redirect_uri),
94 authorization_endpoint: Some(
95 "https://accounts.google.com/o/oauth2/v2/auth".to_string(),
96 ),
97 token_endpoint: Some("https://oauth2.googleapis.com/token".to_string()),
98 scopes: vec![
99 "openid".to_string(),
100 "https://www.googleapis.com/auth/userinfo.profile".to_string(),
101 "https://www.googleapis.com/auth/userinfo.email".to_string(),
102 ],
103 provider: OAuthProvider::Google,
104 }
105 }
106
107 #[must_use]
109 pub fn keycloak(
110 client_id: String,
111 client_secret: String,
112 redirect_uri: String,
113 base_url: &str,
114 realm: &str,
115 ) -> Self {
116 let base = base_url.trim_end_matches('/');
117 Self {
118 enabled: true,
119 client_id: Some(client_id),
120 client_secret: Some(client_secret),
121 redirect_uri: Some(redirect_uri),
122 authorization_endpoint: Some(format!(
123 "{base}/realms/{realm}/protocol/openid-connect/auth"
124 )),
125 token_endpoint: Some(format!(
126 "{base}/realms/{realm}/protocol/openid-connect/token"
127 )),
128 scopes: vec![
129 "openid".to_string(),
130 "profile".to_string(),
131 "email".to_string(),
132 ],
133 provider: OAuthProvider::Keycloak,
134 }
135 }
136
137 pub fn validate(&self) -> Result<()> {
139 if !self.enabled {
140 return Ok(());
141 }
142
143 if self.client_id.is_none() {
144 return Err(Error::config("client_id", "is required"));
145 }
146
147 if self.client_secret.is_none() {
148 return Err(Error::config("client_secret", "is required"));
149 }
150
151 if self.redirect_uri.is_none() {
152 return Err(Error::config("redirect_uri", "is required"));
153 }
154
155 if self.authorization_endpoint.is_none() {
156 return Err(Error::config("authorization_endpoint", "is required"));
157 }
158
159 if self.token_endpoint.is_none() {
160 return Err(Error::config("token_endpoint", "is required"));
161 }
162
163 if let Some(uri) = &self.redirect_uri {
165 Url::parse(uri)
166 .map_err(|e| Error::config("redirect_uri", format!("Invalid URL: {e}")))?;
167 }
168
169 if let Some(endpoint) = &self.authorization_endpoint {
170 Url::parse(endpoint).map_err(|e| {
171 Error::config("authorization_endpoint", format!("Invalid URL: {e}"))
172 })?;
173 }
174
175 if let Some(endpoint) = &self.token_endpoint {
176 Url::parse(endpoint)
177 .map_err(|e| Error::config("token_endpoint", format!("Invalid URL: {e}")))?;
178 }
179
180 Ok(())
181 }
182
183 #[cfg(feature = "auth")]
185 pub fn to_mcp_config(&self) -> Result<()> {
186 if !self.enabled {
187 return Err(Error::config("oauth", "is not enabled"));
188 }
189
190 Ok(())
192 }
193
194 #[cfg(not(feature = "auth"))]
196 pub fn to_mcp_config(&self) -> Result<()> {
197 Err(Error::config("oauth", "feature is not enabled"))
198 }
199}
200
201#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
203#[cfg(feature = "api-key")]
204pub struct ApiKeyConfig {
205 #[serde(default)]
207 pub enabled: bool,
208 #[serde(default)]
215 pub keys: Vec<String>,
216 #[serde(default = "default_header_name")]
218 pub header_name: String,
219 #[serde(default = "default_query_param_name")]
221 pub query_param_name: String,
222 #[serde(default)]
224 pub allow_query_param: bool,
225 #[serde(default = "default_key_prefix")]
227 pub key_prefix: String,
228}
229
230#[cfg(feature = "api-key")]
231fn default_header_name() -> String {
232 "X-API-Key".to_string()
233}
234
235#[cfg(feature = "api-key")]
236fn default_query_param_name() -> String {
237 "api_key".to_string()
238}
239
240#[cfg(feature = "api-key")]
241fn default_key_prefix() -> String {
242 "sk".to_string()
243}
244
245#[cfg(feature = "api-key")]
246impl Default for ApiKeyConfig {
247 fn default() -> Self {
248 Self {
249 enabled: false,
250 keys: Vec::new(),
251 header_name: default_header_name(),
252 query_param_name: default_query_param_name(),
253 allow_query_param: false,
254 key_prefix: default_key_prefix(),
255 }
256 }
257}
258
259#[cfg(feature = "api-key")]
260impl ApiKeyConfig {
261 fn manager(&self) -> Result<ApiKeyManagerV0> {
262 ApiKeyManagerV0::init_default_config(self.key_prefix.clone())
263 .map_err(|e| Error::initialization("api_key_manager", e.to_string()))
264 }
265
266 fn legacy_manager(&self) -> Result<ApiKeyManagerV0> {
267 ApiKeyManagerV0::init(
268 self.key_prefix.clone(),
269 KeyConfig::default().disable_checksum(),
270 HashConfig::default(),
271 std::time::Duration::from_secs(10),
272 )
273 .map_err(|e| Error::initialization("api_key_manager", e.to_string()))
274 }
275
276 fn looks_like_hash(value: &str) -> bool {
277 value.starts_with("$argon2")
278 }
279
280 fn looks_like_legacy_hash(value: &str) -> bool {
281 value.starts_with("legacy:$argon2")
282 }
283
284 fn verify_plaintext_fallback(key: &str, stored_key: &str) -> bool {
285 use api_keys_simplified::SecureStringExt;
286
287 let provided = SecureString::from(key.to_string());
288 let expected = SecureString::from(stored_key.to_string());
289
290 provided.eq(&expected)
291 }
292
293 fn hash_legacy_key(&self, key: &str) -> Result<String> {
294 let manager = self.legacy_manager()?;
295 let seed = self.generate_key()?;
296 let secure = SecureString::from(key.to_string());
297 let hasher = manager.hasher();
298 let api_key = ApiKey::new(secure)
299 .into_hashed_with_phc(hasher, &seed.hash)
300 .map_err(|e| Error::initialization("api_key_hashing", e.to_string()))?;
301 Ok(format!("legacy:{}", api_key.expose_hash().hash()))
302 }
303
304 pub fn validate(&self) -> Result<()> {
306 if !self.enabled {
307 return Ok(());
308 }
309
310 if self.keys.is_empty() {
311 tracing::warn!("API key authentication is enabled but no keys are configured");
312 }
313
314 if self.header_name.is_empty() {
315 return Err(Error::config("header_name", "cannot be empty"));
316 }
317
318 if self.allow_query_param && self.query_param_name.is_empty() {
319 return Err(Error::config(
320 "query_param_name",
321 "cannot be empty when allow_query_param is true",
322 ));
323 }
324
325 if self.key_prefix.is_empty() {
326 return Err(Error::config("key_prefix", "cannot be empty"));
327 }
328
329 let _ = self.manager()?;
330
331 Ok(())
332 }
333
334 #[must_use]
336 pub fn is_valid_key(&self, key: &str) -> bool {
337 if !self.enabled {
338 return true;
339 }
340
341 let manager = self.manager().ok();
342 let legacy_manager = self.legacy_manager().ok();
343 let provided_key = SecureString::from(key.to_string());
344
345 self.keys.iter().fold(false, |found, stored| {
350 let matched = if Self::looks_like_legacy_hash(stored) {
351 if let Some(legacy_manager) = &legacy_manager {
352 let stored_hash = stored.trim_start_matches("legacy:");
353 matches!(
354 legacy_manager.verify(&provided_key, stored_hash),
355 Ok(KeyStatus::Valid)
356 )
357 } else {
358 false
359 }
360 } else if Self::looks_like_hash(stored) {
361 if let Some(manager) = &manager {
362 matches!(manager.verify(&provided_key, stored), Ok(KeyStatus::Valid))
363 } else {
364 false
365 }
366 } else {
367 Self::verify_plaintext_fallback(key, stored)
368 };
369 found | matched
370 })
371 }
372
373 pub fn generate_key(&self) -> Result<GeneratedApiKey> {
382 let manager = self.manager()?;
383
384 let key = manager
385 .generate(Environment::production())
386 .map_err(|e| Error::initialization("api_key_generation", e.to_string()))?;
387
388 Ok(GeneratedApiKey {
389 key: key.key().expose_secret().to_string(),
390 key_id: key.expose_hash().key_id().to_owned(),
391 hash: key.expose_hash().hash().to_owned(),
392 })
393 }
394
395 pub fn normalize_key_material(&self, key: &str) -> Result<String> {
405 if Self::looks_like_hash(key) || Self::looks_like_legacy_hash(key) {
406 Ok(key.to_string())
407 } else {
408 self.hash_legacy_key(key)
409 }
410 }
411}
412
413#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]
415pub struct AuthConfig {
416 #[serde(default)]
418 pub oauth: OAuthConfig,
419 #[cfg(feature = "api-key")]
421 #[serde(default)]
422 pub api_key: ApiKeyConfig,
423}
424
425impl AuthConfig {
426 pub fn validate(&self) -> Result<()> {
428 self.oauth.validate()?;
429 #[cfg(feature = "api-key")]
430 self.api_key.validate()?;
431 Ok(())
432 }
433
434 #[must_use]
436 #[cfg(feature = "api-key")]
437 pub fn is_enabled(&self) -> bool {
438 self.oauth.enabled || self.api_key.enabled
439 }
440
441 #[must_use]
443 #[cfg(not(feature = "api-key"))]
444 pub fn is_enabled(&self) -> bool {
445 self.oauth.enabled
446 }
447}