1use base64::{Engine as _, engine::general_purpose};
30use rand::{Rng, distributions::Alphanumeric};
31use sha2::{Digest, Sha256};
32
33pub 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
63pub 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 assert!(
131 !challenge.contains('='),
132 "Code challenge should not contain padding (=) characters"
133 );
134
135 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 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 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 let expected_length = 43; 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 let (verifier, challenge_result) = generate();
281
282 assert!(
285 verifier.len() >= 43 && verifier.len() <= 128,
286 "Code verifier length should comply with RFC 7636 (43-128 characters)"
287 );
288
289 assert!(
292 verifier.chars().all(|c| {
293 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".contains(c)
294 }),
295 "Code verifier should use RFC 7636 compliant character set"
296 );
297
298 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 let (code_verifier, code_challenge) = generate();
313
314 assert!(!code_challenge.is_empty());
317
318 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 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}