server/auth/
sas_token_generator.rs

1use crate::service_bus_manager::ServiceBusError;
2use base64::{Engine as _, engine::general_purpose};
3use chrono::{Duration, Utc};
4use hmac::{Hmac, Mac};
5use sha2::Sha256;
6
7type HmacSha256 = Hmac<Sha256>;
8
9/// Generator for Azure Service Bus Shared Access Signature (SAS) tokens.
10///
11/// Creates time-limited authentication tokens using HMAC-SHA256 signing with
12/// shared access keys. SAS tokens provide a secure way to grant limited access
13/// to Service Bus resources without sharing the primary keys.
14///
15/// # Security Notes
16///
17/// - Generated tokens have configurable expiration times
18/// - Uses HMAC-SHA256 for cryptographic signing
19/// - Keys are base64 decoded before use in signing
20/// - Tokens include URL-encoded resource URIs for security
21///
22/// # Examples
23///
24/// ```no_run
25/// use quetty_server::auth::SasTokenGenerator;
26///
27/// let generator = SasTokenGenerator::new("my-namespace".to_string());
28/// let token = generator.generate_sas_token(
29///     "RootManageSharedAccessKey",
30///     "base64_encoded_key",
31///     24 // 24 hours
32/// )?;
33/// ```
34#[derive(Clone)]
35pub struct SasTokenGenerator {
36    namespace: String,
37}
38
39impl SasTokenGenerator {
40    /// Creates a new SAS token generator for the specified Service Bus namespace.
41    ///
42    /// # Arguments
43    ///
44    /// * `namespace` - The Service Bus namespace (without .servicebus.windows.net)
45    ///
46    /// # Examples
47    ///
48    /// ```no_run
49    /// use quetty_server::auth::SasTokenGenerator;
50    ///
51    /// let generator = SasTokenGenerator::new("my-servicebus-namespace".to_string());
52    /// ```
53    pub fn new(namespace: String) -> Self {
54        Self { namespace }
55    }
56
57    /// Generates a SAS token for Service Bus authentication.
58    ///
59    /// Creates a time-limited Shared Access Signature token using HMAC-SHA256
60    /// signing with the provided shared access key. The token grants access to
61    /// the entire Service Bus namespace.
62    ///
63    /// # Arguments
64    ///
65    /// * `key_name` - The name of the shared access key policy
66    /// * `key` - The base64-encoded shared access key
67    /// * `duration_hours` - Token validity period in hours
68    ///
69    /// # Returns
70    ///
71    /// A complete SAS token string ready for use in Service Bus operations
72    ///
73    /// # Errors
74    ///
75    /// Returns [`ServiceBusError::AuthenticationError`] if:
76    /// - The key cannot be base64 decoded
77    /// - HMAC generation fails
78    /// - Token signing fails
79    ///
80    /// # Examples
81    ///
82    /// ```no_run
83    /// use quetty_server::auth::SasTokenGenerator;
84    ///
85    /// let generator = SasTokenGenerator::new("namespace".to_string());
86    /// let token = generator.generate_sas_token(
87    ///     "RootManageSharedAccessKey",
88    ///     "base64_encoded_key_here",
89    ///     24 // Valid for 24 hours
90    /// )?;
91    /// ```
92    pub fn generate_sas_token(
93        &self,
94        key_name: &str,
95        key: &str,
96        duration_hours: i64,
97    ) -> Result<String, ServiceBusError> {
98        let expiry = Utc::now() + Duration::hours(duration_hours);
99        let expiry_timestamp = expiry.timestamp();
100
101        let resource_uri = format!("sb://{}.servicebus.windows.net/", self.namespace);
102        let string_to_sign = format!(
103            "{}\n{}",
104            urlencoding::encode(&resource_uri),
105            expiry_timestamp
106        );
107
108        let key_bytes = general_purpose::STANDARD.decode(key).map_err(|e| {
109            ServiceBusError::AuthenticationError(format!("Failed to decode key: {e}"))
110        })?;
111
112        let mut mac = HmacSha256::new_from_slice(&key_bytes).map_err(|e| {
113            ServiceBusError::AuthenticationError(format!("Failed to create HMAC: {e}"))
114        })?;
115
116        mac.update(string_to_sign.as_bytes());
117        let signature = mac.finalize();
118        let signature_base64 = general_purpose::STANDARD.encode(signature.into_bytes());
119
120        let sas_token = format!(
121            "SharedAccessSignature sr={}&sig={}&se={}&skn={}",
122            urlencoding::encode(&resource_uri),
123            urlencoding::encode(&signature_base64),
124            expiry_timestamp,
125            key_name
126        );
127
128        Ok(sas_token)
129    }
130
131    /// Creates a Service Bus connection string from a SAS token.
132    ///
133    /// Combines the namespace endpoint with the SAS token to create a complete
134    /// connection string that can be used for Service Bus operations.
135    ///
136    /// # Arguments
137    ///
138    /// * `sas_token` - A valid SAS token (typically from [`generate_sas_token`])
139    ///
140    /// # Returns
141    ///
142    /// A complete Service Bus connection string
143    ///
144    /// # Examples
145    ///
146    /// ```no_run
147    /// use quetty_server::auth::SasTokenGenerator;
148    ///
149    /// let generator = SasTokenGenerator::new("namespace".to_string());
150    /// let token = generator.generate_sas_token("key_name", "key", 24)?;
151    /// let connection_string = generator.create_connection_string_from_sas(&token);
152    /// ```
153    pub fn create_connection_string_from_sas(&self, sas_token: &str) -> String {
154        format!(
155            "Endpoint=sb://{}.servicebus.windows.net/;SharedAccessSignature={}",
156            self.namespace, sas_token
157        )
158    }
159}