server/auth/types.rs
1use crate::encryption::{ClientSecretEncryption, ConnectionStringEncryption, EncryptionError};
2use serde::{Deserialize, Serialize};
3use std::time::{Duration, Instant};
4
5/// Authentication method types supported by the application.
6///
7/// This enum defines the different ways the application can authenticate
8/// with Azure Service Bus resources.
9#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
10#[serde(rename_all = "snake_case")]
11pub enum AuthType {
12 /// Direct connection using Service Bus connection string
13 ConnectionString,
14 /// Azure Active Directory authentication (Device Code or Client Credentials)
15 AzureAd,
16}
17
18/// Complete authentication configuration for the application.
19///
20/// This struct contains all authentication-related settings including
21/// the primary authentication method and fallback options.
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct AuthConfig {
24 /// The primary authentication method to use
25 pub primary_method: AuthType,
26 /// Whether to enable fallback to alternative authentication methods
27 pub fallback_enabled: bool,
28 /// Connection string configuration (if using ConnectionString auth)
29 pub connection_string: Option<ConnectionStringConfig>,
30 /// Azure AD authentication configuration (if using AzureAd auth)
31 pub azure_ad: Option<AzureAdAuthConfig>,
32}
33
34impl AuthConfig {
35 /// Returns true if any encrypted data is present in this config
36 pub fn has_encrypted_data(&self) -> bool {
37 let connection_string_encrypted = self
38 .connection_string
39 .as_ref()
40 .map(|cs| cs.is_encrypted())
41 .unwrap_or(false);
42
43 let azure_ad_encrypted = self
44 .azure_ad
45 .as_ref()
46 .map(|ad| ad.has_encrypted_data())
47 .unwrap_or(false);
48
49 connection_string_encrypted || azure_ad_encrypted
50 }
51
52 /// Returns a list of authentication methods that require password decryption
53 pub fn get_encrypted_auth_methods(&self) -> Vec<String> {
54 let mut methods = Vec::new();
55
56 if let Some(cs) = &self.connection_string {
57 if cs.is_encrypted() {
58 methods.push("Connection String".to_string());
59 }
60 }
61
62 if let Some(ad) = &self.azure_ad {
63 if ad.has_encrypted_client_secret() {
64 methods.push("Azure AD Client Secret".to_string());
65 }
66 }
67
68 methods
69 }
70}
71
72/// Configuration for connection string authentication.
73///
74/// Contains the Service Bus connection string used for direct authentication
75/// using Shared Access Signatures (SAS). This is the simplest authentication
76/// method but requires managing connection strings securely.
77///
78/// # Required Fields
79///
80/// - `value` - Complete Azure Service Bus connection string with access credentials
81///
82/// # Connection String Format
83///
84/// The connection string must include:
85/// - `Endpoint` - Service Bus namespace endpoint
86/// - `SharedAccessKeyName` - Name of the shared access key
87/// - `SharedAccessKey` - The shared access key value
88///
89/// # Examples
90///
91/// ```no_run
92/// use quetty_server::auth::types::ConnectionStringConfig;
93///
94/// let config = ConnectionStringConfig {
95/// value: "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=abcd1234...".to_string(),
96/// };
97/// ```
98///
99/// # Security Considerations
100///
101/// - Store connection strings securely (environment variables, key vault, etc.)
102/// - Use principle of least privilege - avoid "RootManageSharedAccessKey" in production
103/// - Rotate access keys regularly
104/// - Consider using Azure AD authentication for enhanced security
105#[derive(Clone, Debug, Serialize, Deserialize, Default)]
106pub struct ConnectionStringConfig {
107 /// The Azure Service Bus connection string (REQUIRED)
108 /// Must include Endpoint, SharedAccessKeyName, and SharedAccessKey
109 pub value: String,
110 /// Encrypted connection string (alternative to value)
111 pub encrypted_value: Option<String>,
112 /// Salt for connection string encryption (required when encrypted_value is used)
113 pub encryption_salt: Option<String>,
114}
115
116impl ConnectionStringConfig {
117 /// Returns the actual connection string, decrypting if necessary
118 pub fn get_connection_string(&self, password: Option<&str>) -> Result<String, EncryptionError> {
119 // If we have an encrypted value, decrypt it
120 if let (Some(encrypted), Some(salt)) = (&self.encrypted_value, &self.encryption_salt) {
121 let password = password.ok_or_else(|| {
122 EncryptionError::InvalidData(
123 "Password required for encrypted connection string".to_string(),
124 )
125 })?;
126
127 let encryption = ConnectionStringEncryption::from_salt_base64(salt)?;
128 encryption.decrypt_connection_string(encrypted, password)
129 } else {
130 // Return plain text value
131 Ok(self.value.clone())
132 }
133 }
134
135 /// Returns true if this config contains encrypted data
136 pub fn is_encrypted(&self) -> bool {
137 self.encrypted_value.is_some() && self.encryption_salt.is_some()
138 }
139
140 /// Encrypts the connection string with the given password
141 pub fn encrypt_with_password(&mut self, password: &str) -> Result<(), EncryptionError> {
142 if self.value.trim().is_empty() {
143 return Err(EncryptionError::InvalidData(
144 "Connection string cannot be empty".to_string(),
145 ));
146 }
147
148 let encryption = ConnectionStringEncryption::new();
149 let encrypted = encryption.encrypt_connection_string(&self.value, password)?;
150
151 self.encrypted_value = Some(encrypted);
152 self.encryption_salt = Some(encryption.salt_base64());
153
154 // Clear the plain text value for security
155 self.value.clear();
156
157 Ok(())
158 }
159}
160
161/// Configuration for Azure Active Directory authentication.
162///
163/// Contains all necessary parameters for Azure AD authentication flows
164/// including Device Code Flow and Client Credentials Flow.
165///
166/// # Required Fields
167///
168/// - `auth_method` - Must be "device_code" or "client_secret"
169///
170/// # Required for Device Code Flow
171///
172/// - `tenant_id` - Azure AD tenant ID
173/// - `client_id` - Azure AD application (client) ID
174///
175/// # Required for Client Credentials Flow
176///
177/// - `tenant_id` - Azure AD tenant ID
178/// - `client_id` - Azure AD application (client) ID
179/// - `client_secret` - Azure AD application client secret
180///
181/// # Optional Fields
182///
183/// - `subscription_id` - For resource discovery (defaults to env AZURE_SUBSCRIPTION_ID)
184/// - `resource_group` - For resource discovery (defaults to auto-discovery)
185/// - `namespace` - Service Bus namespace (defaults to auto-discovery)
186/// - `authority_host` - Azure AD authority host (defaults to https://login.microsoftonline.com)
187/// - `scope` - OAuth scope (defaults to https://servicebus.azure.net/.default)
188///
189/// # Examples
190///
191/// ## Device Code Flow Configuration
192/// ```no_run
193/// use quetty_server::auth::types::AzureAdAuthConfig;
194///
195/// let config = AzureAdAuthConfig {
196/// auth_method: "device_code".to_string(),
197/// tenant_id: Some("your-tenant-id".to_string()),
198/// client_id: Some("your-client-id".to_string()),
199/// client_secret: None, // Not needed for device code flow
200/// subscription_id: Some("your-subscription-id".to_string()),
201/// resource_group: Some("your-resource-group".to_string()),
202/// namespace: Some("your-servicebus-namespace".to_string()),
203/// authority_host: None, // Uses default
204/// scope: None, // Uses default
205/// };
206/// ```
207///
208/// ## Client Credentials Flow Configuration
209/// ```no_run
210/// use quetty_server::auth::types::AzureAdAuthConfig;
211///
212/// let config = AzureAdAuthConfig {
213/// auth_method: "client_secret".to_string(),
214/// tenant_id: Some("your-tenant-id".to_string()),
215/// client_id: Some("your-client-id".to_string()),
216/// client_secret: Some("your-client-secret".to_string()), // Required
217/// subscription_id: None, // Optional
218/// resource_group: None, // Optional
219/// namespace: None, // Optional
220/// authority_host: None, // Uses default
221/// scope: None, // Uses default
222/// };
223/// ```
224#[derive(Clone, Debug, Serialize, Deserialize, Default)]
225pub struct AzureAdAuthConfig {
226 /// Authentication method: "device_code" or "client_secret" (REQUIRED)
227 #[serde(default = "default_auth_method")]
228 pub auth_method: String,
229 /// Azure AD tenant ID (REQUIRED for all flows)
230 pub tenant_id: Option<String>,
231 /// Azure AD application (client) ID (REQUIRED for all flows)
232 pub client_id: Option<String>,
233 /// Azure AD application client secret (REQUIRED for client_secret flow)
234 pub client_secret: Option<String>,
235 /// Encrypted client secret (alternative to client_secret)
236 pub encrypted_client_secret: Option<String>,
237 /// Salt for client secret encryption (required when encrypted_client_secret is used)
238 pub client_secret_encryption_salt: Option<String>,
239 /// Azure subscription ID (OPTIONAL - defaults to env AZURE_SUBSCRIPTION_ID)
240 pub subscription_id: Option<String>,
241 /// Resource group name (OPTIONAL - defaults to auto-discovery)
242 pub resource_group: Option<String>,
243 /// Service Bus namespace name (OPTIONAL - defaults to auto-discovery)
244 pub namespace: Option<String>,
245 /// Azure AD authority host URL (OPTIONAL - defaults to https://login.microsoftonline.com)
246 pub authority_host: Option<String>,
247 /// OAuth scope for token requests (OPTIONAL - defaults to https://servicebus.azure.net/.default)
248 pub scope: Option<String>,
249}
250
251fn default_auth_method() -> String {
252 "device_code".to_string()
253}
254
255impl AzureAdAuthConfig {
256 /// Returns the actual client secret, decrypting if necessary
257 pub fn get_client_secret(
258 &self,
259 password: Option<&str>,
260 ) -> Result<Option<String>, EncryptionError> {
261 // If we have an encrypted client secret, decrypt it
262 if let (Some(encrypted), Some(salt)) = (
263 &self.encrypted_client_secret,
264 &self.client_secret_encryption_salt,
265 ) {
266 let password = password.ok_or_else(|| {
267 EncryptionError::InvalidData(
268 "Password required for encrypted client secret".to_string(),
269 )
270 })?;
271
272 let encryption = ClientSecretEncryption::from_salt_base64(salt)?;
273 let decrypted = encryption.decrypt_client_secret(encrypted, password)?;
274 Ok(Some(decrypted))
275 } else {
276 // Return plain text client secret
277 Ok(self.client_secret.clone())
278 }
279 }
280
281 /// Returns true if this config contains encrypted client secret
282 pub fn has_encrypted_client_secret(&self) -> bool {
283 self.encrypted_client_secret.is_some() && self.client_secret_encryption_salt.is_some()
284 }
285
286 /// Returns true if any encrypted data is present in this config
287 pub fn has_encrypted_data(&self) -> bool {
288 self.has_encrypted_client_secret()
289 }
290
291 /// Encrypts the client secret with the given password
292 pub fn encrypt_client_secret_with_password(
293 &mut self,
294 password: &str,
295 ) -> Result<(), EncryptionError> {
296 let client_secret = match &self.client_secret {
297 Some(secret) if !secret.trim().is_empty() => secret,
298 _ => {
299 return Err(EncryptionError::InvalidData(
300 "Client secret cannot be empty".to_string(),
301 ));
302 }
303 };
304
305 let encryption = ClientSecretEncryption::new();
306 let encrypted = encryption.encrypt_client_secret(client_secret, password)?;
307
308 self.encrypted_client_secret = Some(encrypted);
309 self.client_secret_encryption_salt = Some(encryption.salt_base64());
310
311 // Clear the plain text value for security
312 self.client_secret = None;
313
314 Ok(())
315 }
316}
317
318/// A cached authentication token with expiration tracking.
319///
320/// This struct holds an authentication token along with its expiration time
321/// to enable efficient token caching and refresh logic.
322#[derive(Clone, Debug)]
323pub struct CachedToken {
324 /// The authentication token string
325 pub token: String,
326 /// When the token expires
327 pub expires_at: Instant,
328 /// The type of token (e.g., "Bearer")
329 pub token_type: String,
330}
331
332impl CachedToken {
333 /// Creates a new cached token with the given parameters.
334 ///
335 /// # Arguments
336 ///
337 /// * `token` - The authentication token string
338 /// * `expires_in` - Duration until the token expires
339 /// * `token_type` - The type of token (e.g., "Bearer")
340 pub fn new(token: String, expires_in: Duration, token_type: String) -> Self {
341 Self {
342 token,
343 expires_at: Instant::now() + expires_in,
344 token_type,
345 }
346 }
347
348 /// Checks if the token has expired.
349 ///
350 /// # Returns
351 ///
352 /// `true` if the token has passed its expiration time, `false` otherwise.
353 pub fn is_expired(&self) -> bool {
354 Instant::now() >= self.expires_at
355 }
356
357 /// Checks if the token needs to be refreshed soon.
358 ///
359 /// Uses a 5-minute buffer before expiration to ensure tokens are
360 /// refreshed before they actually expire.
361 ///
362 /// # Returns
363 ///
364 /// `true` if the token should be refreshed, `false` otherwise.
365 pub fn needs_refresh(&self) -> bool {
366 let buffer = Duration::from_secs(300); // 5 minute buffer
367 Instant::now() + buffer >= self.expires_at
368 }
369}
370
371/// Information required for Azure AD Device Code Flow authentication.
372///
373/// This struct contains the user code and verification URL that the user
374/// needs to complete the device code authentication flow.
375#[derive(Clone, Debug, Serialize, Deserialize)]
376pub struct DeviceCodeInfo {
377 /// The user code to be entered on the verification page
378 pub user_code: String,
379 /// The URL where the user should enter the code
380 pub verification_uri: String,
381 /// Human-readable message with authentication instructions
382 pub message: String,
383}