Skip to main content

oauth_provider_rs/
secure_string.rs

1use anyhow::{Result, anyhow};
2use ring::rand::SecureRandom;
3
4/// Secure string generator for OAuth tokens, client IDs, and secrets
5pub struct SecureStringGenerator {
6    rng: ring::rand::SystemRandom,
7}
8
9/// Constants for string generation lengths (matching TypeScript implementation)
10pub const TOKEN_LENGTH: usize = 32;
11pub const CLIENT_ID_LENGTH: usize = 16;
12pub const CLIENT_SECRET_LENGTH: usize = 32;
13pub const AUTH_CODE_LENGTH: usize = 32;
14pub const GRANT_ID_LENGTH: usize = 16;
15
16/// Character set for secure string generation
17/// Matches TypeScript implementation: uppercase letters, lowercase letters, digits
18const CHARACTERS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
19
20impl SecureStringGenerator {
21    /// Create a new secure string generator
22    pub fn new() -> Self {
23        Self {
24            rng: ring::rand::SystemRandom::new(),
25        }
26    }
27
28    /// Generate a cryptographically secure random string of the specified length
29    /// Uses the same character set as the TypeScript implementation
30    pub fn generate_random_string(&self, length: usize) -> Result<String> {
31        if length == 0 {
32            return Ok(String::new());
33        }
34
35        let mut result = String::with_capacity(length);
36        let mut values = vec![0u8; length];
37
38        // Fill the array with cryptographically secure random bytes
39        self.rng
40            .fill(&mut values)
41            .map_err(|_| anyhow!("Failed to generate random bytes"))?;
42
43        // Convert random bytes to characters using modulo operation
44        for value in values {
45            let char_index = value as usize % CHARACTERS.len();
46            result.push(CHARACTERS[char_index] as char);
47        }
48
49        Ok(result)
50    }
51
52    /// Generate a client ID (16 characters)
53    pub fn generate_client_id(&self) -> Result<String> {
54        self.generate_random_string(CLIENT_ID_LENGTH)
55    }
56
57    /// Generate a client secret (32 characters)
58    pub fn generate_client_secret(&self) -> Result<String> {
59        self.generate_random_string(CLIENT_SECRET_LENGTH)
60    }
61
62    /// Generate an access token secret (32 characters)
63    pub fn generate_access_token_secret(&self) -> Result<String> {
64        self.generate_random_string(TOKEN_LENGTH)
65    }
66
67    /// Generate a refresh token secret (32 characters)
68    pub fn generate_refresh_token_secret(&self) -> Result<String> {
69        self.generate_random_string(TOKEN_LENGTH)
70    }
71
72    /// Generate an authorization code secret (32 characters)
73    pub fn generate_auth_code_secret(&self) -> Result<String> {
74        self.generate_random_string(AUTH_CODE_LENGTH)
75    }
76
77    /// Generate a grant ID (16 characters)
78    pub fn generate_grant_id(&self) -> Result<String> {
79        self.generate_random_string(GRANT_ID_LENGTH)
80    }
81
82    /// Generate a PKCE code verifier (43-128 characters as per RFC 7636)
83    /// Default to 43 characters for compatibility
84    pub fn generate_pkce_code_verifier(&self) -> Result<String> {
85        self.generate_random_string(43)
86    }
87
88    /// Generate a state parameter for OAuth flow (recommended 32 characters)
89    pub fn generate_state_parameter(&self) -> Result<String> {
90        self.generate_random_string(32)
91    }
92
93    /// Generate a nonce for OpenID Connect (recommended 32 characters)
94    pub fn generate_nonce(&self) -> Result<String> {
95        self.generate_random_string(32)
96    }
97}
98
99impl Default for SecureStringGenerator {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105/// Global secure string generator instance
106static GLOBAL_GENERATOR: std::sync::LazyLock<SecureStringGenerator> =
107    std::sync::LazyLock::new(|| SecureStringGenerator::new());
108
109/// Generate a cryptographically secure random string using the global generator
110pub fn generate_random_string(length: usize) -> Result<String> {
111    GLOBAL_GENERATOR.generate_random_string(length)
112}
113
114/// Generate a client ID using the global generator
115pub fn generate_client_id() -> Result<String> {
116    GLOBAL_GENERATOR.generate_client_id()
117}
118
119/// Generate a client secret using the global generator
120pub fn generate_client_secret() -> Result<String> {
121    GLOBAL_GENERATOR.generate_client_secret()
122}
123
124/// Generate an access token secret using the global generator
125pub fn generate_access_token_secret() -> Result<String> {
126    GLOBAL_GENERATOR.generate_access_token_secret()
127}
128
129/// Generate a refresh token secret using the global generator
130pub fn generate_refresh_token_secret() -> Result<String> {
131    GLOBAL_GENERATOR.generate_refresh_token_secret()
132}
133
134/// Generate an authorization code secret using the global generator
135pub fn generate_auth_code_secret() -> Result<String> {
136    GLOBAL_GENERATOR.generate_auth_code_secret()
137}
138
139/// Generate a grant ID using the global generator
140pub fn generate_grant_id() -> Result<String> {
141    GLOBAL_GENERATOR.generate_grant_id()
142}
143
144/// Generate a PKCE code verifier using the global generator
145pub fn generate_pkce_code_verifier() -> Result<String> {
146    GLOBAL_GENERATOR.generate_pkce_code_verifier()
147}
148
149/// Generate a state parameter using the global generator
150pub fn generate_state_parameter() -> Result<String> {
151    GLOBAL_GENERATOR.generate_state_parameter()
152}
153
154/// Generate a nonce using the global generator
155pub fn generate_nonce() -> Result<String> {
156    GLOBAL_GENERATOR.generate_nonce()
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use std::collections::HashSet;
163
164    #[test]
165    fn test_generate_random_string() {
166        let generator = SecureStringGenerator::new();
167
168        // Test different lengths
169        for length in [0, 1, 16, 32, 64, 128] {
170            let result = generator.generate_random_string(length).unwrap();
171            assert_eq!(result.len(), length);
172
173            // Verify all characters are from the allowed set
174            for ch in result.chars() {
175                assert!(CHARACTERS.contains(&(ch as u8)));
176            }
177        }
178    }
179
180    #[test]
181    fn test_string_uniqueness() {
182        let generator = SecureStringGenerator::new();
183        let mut strings = HashSet::new();
184
185        // Generate 1000 strings and verify they're all unique
186        for _ in 0..1000 {
187            let s = generator.generate_random_string(32).unwrap();
188            assert!(strings.insert(s), "Generated duplicate string");
189        }
190    }
191
192    #[test]
193    fn test_character_set_validation() {
194        let generator = SecureStringGenerator::new();
195        let result = generator.generate_random_string(10000).unwrap();
196
197        let mut found_uppercase = false;
198        let mut found_lowercase = false;
199        let mut found_digit = false;
200
201        for ch in result.chars() {
202            if ch.is_ascii_uppercase() {
203                found_uppercase = true;
204            } else if ch.is_ascii_lowercase() {
205                found_lowercase = true;
206            } else if ch.is_ascii_digit() {
207                found_digit = true;
208            }
209
210            // Verify character is in allowed set
211            assert!(ch.is_ascii_alphanumeric());
212        }
213
214        // With 10000 characters, we should have found all types
215        assert!(found_uppercase);
216        assert!(found_lowercase);
217        assert!(found_digit);
218    }
219
220    #[test]
221    fn test_specific_length_generators() {
222        let generator = SecureStringGenerator::new();
223
224        // Test client ID generation
225        let client_id = generator.generate_client_id().unwrap();
226        assert_eq!(client_id.len(), CLIENT_ID_LENGTH);
227
228        // Test client secret generation
229        let client_secret = generator.generate_client_secret().unwrap();
230        assert_eq!(client_secret.len(), CLIENT_SECRET_LENGTH);
231
232        // Test access token secret generation
233        let access_token = generator.generate_access_token_secret().unwrap();
234        assert_eq!(access_token.len(), TOKEN_LENGTH);
235
236        // Test refresh token secret generation
237        let refresh_token = generator.generate_refresh_token_secret().unwrap();
238        assert_eq!(refresh_token.len(), TOKEN_LENGTH);
239
240        // Test authorization code secret generation
241        let auth_code = generator.generate_auth_code_secret().unwrap();
242        assert_eq!(auth_code.len(), AUTH_CODE_LENGTH);
243
244        // Test grant ID generation
245        let grant_id = generator.generate_grant_id().unwrap();
246        assert_eq!(grant_id.len(), GRANT_ID_LENGTH);
247    }
248
249    #[test]
250    fn test_pkce_code_verifier_generation() {
251        let generator = SecureStringGenerator::new();
252        let verifier = generator.generate_pkce_code_verifier().unwrap();
253
254        // PKCE code verifier should be 43-128 characters
255        assert_eq!(verifier.len(), 43);
256
257        // Verify all characters are URL-safe
258        for ch in verifier.chars() {
259            assert!(ch.is_ascii_alphanumeric());
260        }
261    }
262
263    #[test]
264    fn test_global_generator_functions() {
265        // Test all global generator functions
266        let client_id = generate_client_id().unwrap();
267        assert_eq!(client_id.len(), CLIENT_ID_LENGTH);
268
269        let client_secret = generate_client_secret().unwrap();
270        assert_eq!(client_secret.len(), CLIENT_SECRET_LENGTH);
271
272        let access_token = generate_access_token_secret().unwrap();
273        assert_eq!(access_token.len(), TOKEN_LENGTH);
274
275        let refresh_token = generate_refresh_token_secret().unwrap();
276        assert_eq!(refresh_token.len(), TOKEN_LENGTH);
277
278        let auth_code = generate_auth_code_secret().unwrap();
279        assert_eq!(auth_code.len(), AUTH_CODE_LENGTH);
280
281        let grant_id = generate_grant_id().unwrap();
282        assert_eq!(grant_id.len(), GRANT_ID_LENGTH);
283
284        let state = generate_state_parameter().unwrap();
285        assert_eq!(state.len(), 32);
286
287        let nonce = generate_nonce().unwrap();
288        assert_eq!(nonce.len(), 32);
289    }
290
291    #[test]
292    fn test_empty_string_generation() {
293        let generator = SecureStringGenerator::new();
294        let result = generator.generate_random_string(0).unwrap();
295        assert!(result.is_empty());
296    }
297
298    #[test]
299    fn test_distribution_fairness() {
300        let generator = SecureStringGenerator::new();
301        let mut char_counts = [0; 62]; // 62 characters in our set
302
303        // Generate a large string to test distribution
304        let large_string = generator.generate_random_string(62000).unwrap();
305
306        for ch in large_string.chars() {
307            let char_byte = ch as u8;
308            if let Some(index) = CHARACTERS.iter().position(|&x| x == char_byte) {
309                char_counts[index] += 1;
310            }
311        }
312
313        // Check that each character appears at least once
314        // With 62000 characters and 62 possible characters, we expect ~1000 of each
315        // Allow for some variance in random distribution
316        for count in char_counts.iter() {
317            assert!(*count > 500, "Character distribution too uneven: {}", count);
318            assert!(
319                *count < 1500,
320                "Character distribution too uneven: {}",
321                count
322            );
323        }
324    }
325
326    #[test]
327    fn test_multiple_generators() {
328        // Test that multiple generators produce different results
329        let gen1 = SecureStringGenerator::new();
330        let gen2 = SecureStringGenerator::new();
331
332        let s1 = gen1.generate_random_string(32).unwrap();
333        let s2 = gen2.generate_random_string(32).unwrap();
334
335        // They should be different (extremely high probability)
336        assert_ne!(s1, s2);
337    }
338
339    #[test]
340    fn test_constants_match_typescript() {
341        // Verify constants match TypeScript implementation
342        assert_eq!(TOKEN_LENGTH, 32);
343        assert_eq!(CLIENT_ID_LENGTH, 16);
344        assert_eq!(CLIENT_SECRET_LENGTH, 32);
345        assert_eq!(AUTH_CODE_LENGTH, 32);
346        assert_eq!(GRANT_ID_LENGTH, 16);
347    }
348
349    #[test]
350    fn test_character_set_matches_typescript() {
351        // Verify character set matches TypeScript implementation
352        let expected = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
353        let actual = std::str::from_utf8(CHARACTERS).unwrap();
354        assert_eq!(actual, expected);
355    }
356}