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}