use base64::{Engine as _, engine::general_purpose};
use rand::{Rng, distributions::Alphanumeric};
use sha2::{Digest, Sha256};
pub fn generate() -> (String, String) {
let token: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(100)
.map(char::from)
.collect();
(token.clone(), challenge(&token))
}
pub fn challenge(token: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(token.as_bytes());
let result = hasher.finalize();
general_purpose::URL_SAFE_NO_PAD.encode(result)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_generate_returns_correct_verifier_length() {
let (verifier, _) = generate();
assert_eq!(
verifier.len(),
100,
"Code verifier should be exactly 100 characters"
);
}
#[test]
fn test_generate_verifier_is_alphanumeric() {
let (verifier, _) = generate();
assert!(
verifier.chars().all(|c| c.is_alphanumeric()),
"Code verifier should contain only alphanumeric characters"
);
}
#[test]
fn test_generate_challenge_is_not_empty() {
let (_, challenge) = generate();
assert!(!challenge.is_empty(), "Code challenge should not be empty");
}
#[test]
fn test_generate_challenge_is_base64url_without_padding() {
let (_, challenge) = generate();
assert!(
!challenge.contains('='),
"Code challenge should not contain padding (=) characters"
);
assert!(
challenge
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_'),
"Code challenge should only contain base64url characters (A-Z, a-z, 0-9, -, _)"
);
}
#[test]
fn test_generate_produces_unique_values() {
let mut verifiers = HashSet::new();
let mut challenges = HashSet::new();
for _ in 0..10 {
let (verifier, challenge) = generate();
assert!(
verifiers.insert(verifier.clone()),
"Code verifiers should be unique"
);
assert!(
challenges.insert(challenge.clone()),
"Code challenges should be unique"
);
}
}
#[test]
fn test_generate_verifier_and_challenge_are_related() {
let (verifier, challenge_result) = generate();
let computed_challenge = challenge(&verifier);
assert_eq!(
challenge_result, computed_challenge,
"Generated challenge should match computed challenge from verifier"
);
}
#[test]
fn test_challenge_with_known_input() {
let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
let expected_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
let actual_challenge = challenge(verifier);
assert_eq!(
actual_challenge, expected_challenge,
"Challenge should match RFC 7636 test vector"
);
}
#[test]
fn test_challenge_deterministic() {
let verifier = "test_verifier_123";
let challenge1 = challenge(verifier);
let challenge2 = challenge(verifier);
assert_eq!(
challenge1, challenge2,
"Challenge function should be deterministic"
);
}
#[test]
fn test_challenge_different_inputs_produce_different_outputs() {
let challenge1 = challenge("verifier1");
let challenge2 = challenge("verifier2");
assert_ne!(
challenge1, challenge2,
"Different verifiers should produce different challenges"
);
}
#[test]
fn test_challenge_empty_string() {
let result = challenge("");
assert!(
!result.is_empty(),
"Challenge of empty string should not be empty"
);
assert!(
!result.contains('='),
"Challenge should not contain padding"
);
}
#[test]
fn test_challenge_unicode_input() {
let verifier = "test_émojis_🔐_unicode";
let result = challenge(verifier);
assert!(
!result.is_empty(),
"Challenge should work with unicode input"
);
assert!(
!result.contains('='),
"Challenge should not contain padding"
);
}
#[test]
fn test_challenge_very_long_input() {
let verifier = "a".repeat(1000);
let result = challenge(&verifier);
assert!(
!result.is_empty(),
"Challenge should work with very long input"
);
assert!(
!result.contains('='),
"Challenge should not contain padding"
);
}
#[test]
fn test_challenge_output_length_consistency() {
let expected_length = 43;
let long_string = "a".repeat(100);
let test_inputs = vec![
"",
"short",
&long_string,
"unicode_🔐_test",
"special!@#$%^&*()characters",
];
for input in test_inputs {
let result = challenge(input);
assert_eq!(
result.len(),
expected_length,
"Challenge output length should be consistent for input: '{}'",
input
);
}
}
#[test]
fn test_rfc7636_compliance() {
let (verifier, challenge_result) = generate();
assert!(
verifier.len() >= 43 && verifier.len() <= 128,
"Code verifier length should comply with RFC 7636 (43-128 characters)"
);
assert!(
verifier.chars().all(|c| {
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".contains(c)
}),
"Code verifier should use RFC 7636 compliant character set"
);
assert_eq!(
challenge_result.len(),
43,
"Code challenge should be 43 characters (SHA256 base64url encoded)"
);
}
#[test]
fn test_pkce_flow_simulation() {
let (code_verifier, code_challenge) = generate();
assert!(!code_challenge.is_empty());
assert!(
!code_challenge.contains('='),
"Challenge should not have padding"
);
assert_eq!(
code_challenge.len(),
43,
"Challenge should be correct length"
);
let server_computed_challenge = challenge(&code_verifier);
assert_eq!(
code_challenge, server_computed_challenge,
"Server should be able to verify PKCE parameters"
);
}
}