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}