use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use rand::Rng;
use sha2::{Digest, Sha256};
fn generate_random_string(length: usize) -> String {
let mut rng = rand::thread_rng();
(0..length)
.map(|_| {
let idx = rng.gen_range(0..62);
if idx < 26 {
(b'A' + idx) as char
} else if idx < 52 {
(b'a' + (idx - 26)) as char
} else {
(b'0' + (idx - 52)) as char
}
})
.collect()
}
pub fn generate_pkce() -> (String, String) {
let code_verifier = generate_random_string(128);
let code_challenge = generate_code_challenge(&code_verifier);
(code_verifier, code_challenge)
}
fn generate_code_challenge(code_verifier: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(code_verifier.as_bytes());
let hash = hasher.finalize();
URL_SAFE_NO_PAD.encode(hash)
}
pub fn generate_state() -> String {
generate_random_string(32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_pkce() {
let (verifier, challenge) = generate_pkce();
assert_eq!(verifier.len(), 128);
assert!(!challenge.is_empty());
let expected_challenge = generate_code_challenge(&verifier);
assert_eq!(challenge, expected_challenge);
}
#[test]
fn test_generate_pkce_uniqueness() {
let (verifier1, challenge1) = generate_pkce();
let (verifier2, challenge2) = generate_pkce();
assert_ne!(verifier1, verifier2);
assert_ne!(challenge1, challenge2);
}
#[test]
fn test_generate_state() {
let state = generate_state();
assert_eq!(state.len(), 32);
assert!(state.chars().all(|c| c.is_alphanumeric()));
}
#[test]
fn test_generate_state_uniqueness() {
let state1 = generate_state();
let state2 = generate_state();
assert_ne!(state1, state2);
}
#[test]
fn test_code_challenge_deterministic() {
let verifier = "test_verifier_12345";
let challenge1 = generate_code_challenge(verifier);
let challenge2 = generate_code_challenge(verifier);
assert_eq!(challenge1, challenge2);
}
}