1use base64::{Engine as _, engine::general_purpose};
36use rand::{Rng, distributions::Alphanumeric};
37use sha2::{Digest, Sha256};
38
39pub fn generate() -> (String, String) {
61 let token: String = rand::thread_rng()
62 .sample_iter(&Alphanumeric)
63 .take(100)
64 .map(char::from)
65 .collect();
66 (token.clone(), challenge(&token))
67}
68
69pub fn challenge(token: &str) -> String {
94 let mut hasher = Sha256::new();
95 hasher.update(token.as_bytes());
96 let result = hasher.finalize();
97
98 general_purpose::URL_SAFE_NO_PAD.encode(result)
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use std::collections::HashSet;
105
106 #[test]
107 fn test_generate_returns_correct_verifier_length() {
108 let (verifier, _) = generate();
109 assert_eq!(
110 verifier.len(),
111 100,
112 "Code verifier should be exactly 100 characters"
113 );
114 }
115
116 #[test]
117 fn test_generate_verifier_is_alphanumeric() {
118 let (verifier, _) = generate();
119 assert!(
120 verifier.chars().all(|c| c.is_alphanumeric()),
121 "Code verifier should contain only alphanumeric characters"
122 );
123 }
124
125 #[test]
126 fn test_generate_challenge_is_not_empty() {
127 let (_, challenge) = generate();
128 assert!(!challenge.is_empty(), "Code challenge should not be empty");
129 }
130
131 #[test]
132 fn test_generate_challenge_is_base64url_without_padding() {
133 let (_, challenge) = generate();
134
135 assert!(
137 !challenge.contains('='),
138 "Code challenge should not contain padding (=) characters"
139 );
140
141 assert!(
143 challenge
144 .chars()
145 .all(|c| c.is_alphanumeric() || c == '-' || c == '_'),
146 "Code challenge should only contain base64url characters (A-Z, a-z, 0-9, -, _)"
147 );
148 }
149
150 #[test]
151 fn test_generate_produces_unique_values() {
152 let mut verifiers = HashSet::new();
153 let mut challenges = HashSet::new();
154
155 for _ in 0..10 {
157 let (verifier, challenge) = generate();
158 assert!(
159 verifiers.insert(verifier.clone()),
160 "Code verifiers should be unique"
161 );
162 assert!(
163 challenges.insert(challenge.clone()),
164 "Code challenges should be unique"
165 );
166 }
167 }
168
169 #[test]
170 fn test_generate_verifier_and_challenge_are_related() {
171 let (verifier, challenge_result) = generate();
172 let computed_challenge = challenge(&verifier);
173 assert_eq!(
174 challenge_result, computed_challenge,
175 "Generated challenge should match computed challenge from verifier"
176 );
177 }
178
179 #[test]
180 fn test_challenge_with_known_input() {
181 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
183 let expected_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
184
185 let actual_challenge = challenge(verifier);
186 assert_eq!(
187 actual_challenge, expected_challenge,
188 "Challenge should match RFC 7636 test vector"
189 );
190 }
191
192 #[test]
193 fn test_challenge_deterministic() {
194 let verifier = "test_verifier_123";
195 let challenge1 = challenge(verifier);
196 let challenge2 = challenge(verifier);
197
198 assert_eq!(
199 challenge1, challenge2,
200 "Challenge function should be deterministic"
201 );
202 }
203
204 #[test]
205 fn test_challenge_different_inputs_produce_different_outputs() {
206 let challenge1 = challenge("verifier1");
207 let challenge2 = challenge("verifier2");
208
209 assert_ne!(
210 challenge1, challenge2,
211 "Different verifiers should produce different challenges"
212 );
213 }
214
215 #[test]
216 fn test_challenge_empty_string() {
217 let result = challenge("");
218 assert!(
219 !result.is_empty(),
220 "Challenge of empty string should not be empty"
221 );
222 assert!(
223 !result.contains('='),
224 "Challenge should not contain padding"
225 );
226 }
227
228 #[test]
229 fn test_challenge_unicode_input() {
230 let verifier = "test_émojis_🔐_unicode";
231 let result = challenge(verifier);
232
233 assert!(
234 !result.is_empty(),
235 "Challenge should work with unicode input"
236 );
237 assert!(
238 !result.contains('='),
239 "Challenge should not contain padding"
240 );
241 }
242
243 #[test]
244 fn test_challenge_very_long_input() {
245 let verifier = "a".repeat(1000);
246 let result = challenge(&verifier);
247
248 assert!(
249 !result.is_empty(),
250 "Challenge should work with very long input"
251 );
252 assert!(
253 !result.contains('='),
254 "Challenge should not contain padding"
255 );
256 }
257
258 #[test]
259 fn test_challenge_output_length_consistency() {
260 let expected_length = 43; let long_string = "a".repeat(100);
264 let test_inputs = vec![
265 "",
266 "short",
267 &long_string,
268 "unicode_🔐_test",
269 "special!@#$%^&*()characters",
270 ];
271
272 for input in test_inputs {
273 let result = challenge(input);
274 assert_eq!(
275 result.len(),
276 expected_length,
277 "Challenge output length should be consistent for input: '{}'",
278 input
279 );
280 }
281 }
282
283 #[test]
284 fn test_rfc7636_compliance() {
285 let (verifier, challenge_result) = generate();
287
288 assert!(
291 verifier.len() >= 43 && verifier.len() <= 128,
292 "Code verifier length should comply with RFC 7636 (43-128 characters)"
293 );
294
295 assert!(
298 verifier.chars().all(|c| {
299 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".contains(c)
300 }),
301 "Code verifier should use RFC 7636 compliant character set"
302 );
303
304 assert_eq!(
307 challenge_result.len(),
308 43,
309 "Code challenge should be 43 characters (SHA256 base64url encoded)"
310 );
311 }
312
313 #[test]
314 fn test_pkce_flow_simulation() {
315 let (code_verifier, code_challenge) = generate();
319
320 assert!(!code_challenge.is_empty());
323
324 assert!(
326 !code_challenge.contains('='),
327 "Challenge should not have padding"
328 );
329 assert_eq!(
330 code_challenge.len(),
331 43,
332 "Challenge should be correct length"
333 );
334
335 let server_computed_challenge = challenge(&code_verifier);
338 assert_eq!(
339 code_challenge, server_computed_challenge,
340 "Server should be able to verify PKCE parameters"
341 );
342 }
343}