A library for ATProtocol identities.
1//! PKCE (Proof Key for Code Exchange) implementation. 2//! 3//! RFC 7636 compliant PKCE for OAuth 2.0 authorization code flow 4//! security with SHA256 challenge generation. 5//! 2. **Authorize**: Send the code challenge with the authorization request 6//! 3. **Exchange**: Send the original code verifier when exchanging the authorization code for tokens 7//! 8//! ## Example 9//! 10//! ```rust 11//! use atproto_oauth::pkce; 12//! 13//! // Generate PKCE parameters 14//! let (code_verifier, code_challenge) = pkce::generate(); 15//! 16//! // Use code_challenge in authorization URL 17//! println!("Authorization URL: https://auth.example.com/oauth/authorize?code_challenge={}", code_challenge); 18//! 19//! // Later, use code_verifier when exchanging authorization code for tokens 20//! println!("Token exchange: code_verifier={}", code_verifier); 21//! ``` 22//! 23//! ## Security 24//! 25//! - Code verifiers are generated using cryptographically secure random number generation 26//! - Challenges use SHA256 hashing with base64url encoding (without padding) 27//! - Implements the S256 code challenge method as specified in RFC 7636 28 29use base64::{Engine as _, engine::general_purpose}; 30use rand::{Rng, distributions::Alphanumeric}; 31use sha2::{Digest, Sha256}; 32 33/// Generates a PKCE code verifier and code challenge pair. 34/// 35/// Creates a cryptographically random code verifier (100 characters) and computes 36/// its corresponding SHA256 code challenge. This follows the PKCE specification 37/// in RFC 7636 using the S256 code challenge method. 38/// 39/// # Returns 40/// 41/// A tuple containing: 42/// - `String`: The code verifier (random alphanumeric string) 43/// - `String`: The code challenge (base64url-encoded SHA256 hash of verifier) 44/// 45/// # Example 46/// 47/// ```rust 48/// use atproto_oauth::pkce; 49/// 50/// let (verifier, challenge) = pkce::generate(); 51/// assert_eq!(verifier.len(), 100); 52/// assert!(!challenge.is_empty()); 53/// ``` 54pub fn generate() -> (String, String) { 55 let token: String = rand::thread_rng() 56 .sample_iter(&Alphanumeric) 57 .take(100) 58 .map(char::from) 59 .collect(); 60 (token.clone(), challenge(&token)) 61} 62 63/// Creates a PKCE code challenge from a code verifier. 64/// 65/// Computes the SHA256 hash of the provided code verifier and encodes it using 66/// base64url encoding without padding. This implements the S256 code challenge 67/// method as specified in RFC 7636 section 4.2. 68/// 69/// # Arguments 70/// 71/// * `token` - The code verifier string to hash 72/// 73/// # Returns 74/// 75/// The base64url-encoded SHA256 hash of the code verifier 76/// 77/// # Example 78/// 79/// ```rust 80/// use atproto_oauth::pkce; 81/// 82/// let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; 83/// let challenge = pkce::challenge(verifier); 84/// assert!(!challenge.is_empty()); 85/// assert!(!challenge.contains('=')); // No padding in base64url 86/// ``` 87pub fn challenge(token: &str) -> String { 88 let mut hasher = Sha256::new(); 89 hasher.update(token.as_bytes()); 90 let result = hasher.finalize(); 91 92 general_purpose::URL_SAFE_NO_PAD.encode(result) 93} 94 95#[cfg(test)] 96mod tests { 97 use super::*; 98 use std::collections::HashSet; 99 100 #[test] 101 fn test_generate_returns_correct_verifier_length() { 102 let (verifier, _) = generate(); 103 assert_eq!( 104 verifier.len(), 105 100, 106 "Code verifier should be exactly 100 characters" 107 ); 108 } 109 110 #[test] 111 fn test_generate_verifier_is_alphanumeric() { 112 let (verifier, _) = generate(); 113 assert!( 114 verifier.chars().all(|c| c.is_alphanumeric()), 115 "Code verifier should contain only alphanumeric characters" 116 ); 117 } 118 119 #[test] 120 fn test_generate_challenge_is_not_empty() { 121 let (_, challenge) = generate(); 122 assert!(!challenge.is_empty(), "Code challenge should not be empty"); 123 } 124 125 #[test] 126 fn test_generate_challenge_is_base64url_without_padding() { 127 let (_, challenge) = generate(); 128 129 // Should not contain padding characters 130 assert!( 131 !challenge.contains('='), 132 "Code challenge should not contain padding (=) characters" 133 ); 134 135 // Should only contain valid base64url characters 136 assert!( 137 challenge 138 .chars() 139 .all(|c| c.is_alphanumeric() || c == '-' || c == '_'), 140 "Code challenge should only contain base64url characters (A-Z, a-z, 0-9, -, _)" 141 ); 142 } 143 144 #[test] 145 fn test_generate_produces_unique_values() { 146 let mut verifiers = HashSet::new(); 147 let mut challenges = HashSet::new(); 148 149 // Generate multiple PKCE pairs and ensure they're all unique 150 for _ in 0..10 { 151 let (verifier, challenge) = generate(); 152 assert!( 153 verifiers.insert(verifier.clone()), 154 "Code verifiers should be unique" 155 ); 156 assert!( 157 challenges.insert(challenge.clone()), 158 "Code challenges should be unique" 159 ); 160 } 161 } 162 163 #[test] 164 fn test_generate_verifier_and_challenge_are_related() { 165 let (verifier, challenge_result) = generate(); 166 let computed_challenge = challenge(&verifier); 167 assert_eq!( 168 challenge_result, computed_challenge, 169 "Generated challenge should match computed challenge from verifier" 170 ); 171 } 172 173 #[test] 174 fn test_challenge_with_known_input() { 175 // Test vector from RFC 7636 example 176 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; 177 let expected_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; 178 179 let actual_challenge = challenge(verifier); 180 assert_eq!( 181 actual_challenge, expected_challenge, 182 "Challenge should match RFC 7636 test vector" 183 ); 184 } 185 186 #[test] 187 fn test_challenge_deterministic() { 188 let verifier = "test_verifier_123"; 189 let challenge1 = challenge(verifier); 190 let challenge2 = challenge(verifier); 191 192 assert_eq!( 193 challenge1, challenge2, 194 "Challenge function should be deterministic" 195 ); 196 } 197 198 #[test] 199 fn test_challenge_different_inputs_produce_different_outputs() { 200 let challenge1 = challenge("verifier1"); 201 let challenge2 = challenge("verifier2"); 202 203 assert_ne!( 204 challenge1, challenge2, 205 "Different verifiers should produce different challenges" 206 ); 207 } 208 209 #[test] 210 fn test_challenge_empty_string() { 211 let result = challenge(""); 212 assert!( 213 !result.is_empty(), 214 "Challenge of empty string should not be empty" 215 ); 216 assert!( 217 !result.contains('='), 218 "Challenge should not contain padding" 219 ); 220 } 221 222 #[test] 223 fn test_challenge_unicode_input() { 224 let verifier = "test_émojis_🔐_unicode"; 225 let result = challenge(verifier); 226 227 assert!( 228 !result.is_empty(), 229 "Challenge should work with unicode input" 230 ); 231 assert!( 232 !result.contains('='), 233 "Challenge should not contain padding" 234 ); 235 } 236 237 #[test] 238 fn test_challenge_very_long_input() { 239 let verifier = "a".repeat(1000); 240 let result = challenge(&verifier); 241 242 assert!( 243 !result.is_empty(), 244 "Challenge should work with very long input" 245 ); 246 assert!( 247 !result.contains('='), 248 "Challenge should not contain padding" 249 ); 250 } 251 252 #[test] 253 fn test_challenge_output_length_consistency() { 254 // SHA256 produces 32 bytes, base64url encoding should produce consistent length 255 let expected_length = 43; // 32 bytes -> 43 characters in base64url without padding 256 257 let long_string = "a".repeat(100); 258 let test_inputs = vec![ 259 "", 260 "short", 261 &long_string, 262 "unicode_🔐_test", 263 "special!@#$%^&*()characters", 264 ]; 265 266 for input in test_inputs { 267 let result = challenge(input); 268 assert_eq!( 269 result.len(), 270 expected_length, 271 "Challenge output length should be consistent for input: '{}'", 272 input 273 ); 274 } 275 } 276 277 #[test] 278 fn test_rfc7636_compliance() { 279 // Test that our implementation follows RFC 7636 requirements 280 let (verifier, challenge_result) = generate(); 281 282 // RFC 7636 Section 4.1: code_verifier should be 43-128 characters 283 // Our implementation uses 100 characters 284 assert!( 285 verifier.len() >= 43 && verifier.len() <= 128, 286 "Code verifier length should comply with RFC 7636 (43-128 characters)" 287 ); 288 289 // RFC 7636 Section 4.1: code_verifier should use [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" 290 // Our implementation uses only alphanumeric (subset of allowed characters) 291 assert!( 292 verifier.chars().all(|c| { 293 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".contains(c) 294 }), 295 "Code verifier should use RFC 7636 compliant character set" 296 ); 297 298 // RFC 7636 Section 4.2: code_challenge should be base64url-encoded SHA256 299 // Should be 43 characters (32 bytes SHA256 -> 43 chars base64url without padding) 300 assert_eq!( 301 challenge_result.len(), 302 43, 303 "Code challenge should be 43 characters (SHA256 base64url encoded)" 304 ); 305 } 306 307 #[test] 308 fn test_pkce_flow_simulation() { 309 // Simulate a complete PKCE flow 310 311 // Step 1: Client generates PKCE parameters 312 let (code_verifier, code_challenge) = generate(); 313 314 // Step 2: Client sends code_challenge to authorization server 315 // (In real implementation, this would be in authorization URL) 316 assert!(!code_challenge.is_empty()); 317 318 // Step 3: Authorization server validates challenge format 319 assert!( 320 !code_challenge.contains('='), 321 "Challenge should not have padding" 322 ); 323 assert_eq!( 324 code_challenge.len(), 325 43, 326 "Challenge should be correct length" 327 ); 328 329 // Step 4: Client later sends code_verifier to token endpoint 330 // Server verifies that SHA256(code_verifier) == code_challenge 331 let server_computed_challenge = challenge(&code_verifier); 332 assert_eq!( 333 code_challenge, server_computed_challenge, 334 "Server should be able to verify PKCE parameters" 335 ); 336 } 337}