use base64::{Engine as _, engine::general_purpose};
use rand::{RngExt, distr::Alphanumeric};
use sha2::{Digest, Sha256};
pub fn generate_pkce() -> (String, String) {
let mut code_verifier = String::with_capacity(64);
code_verifier.extend(
rand::rng()
.sample_iter(&Alphanumeric)
.take(64)
.map(char::from),
);
let mut hasher = Sha256::new();
hasher.update(code_verifier.as_bytes());
let result = hasher.finalize();
let code_challenge = general_purpose::URL_SAFE_NO_PAD.encode(result);
(code_verifier, code_challenge)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_pkce_length() {
let (verifier, _) = generate_pkce();
assert_eq!(
verifier.len(),
64,
"Code verifier should be 64 characters long"
);
}
#[test]
fn test_generate_pkce_challenge_format() {
let (verifier, challenge) = generate_pkce();
let mut hasher = Sha256::new();
hasher.update(verifier.as_bytes());
let result = hasher.finalize();
let expected_challenge = general_purpose::URL_SAFE_NO_PAD.encode(result);
assert_eq!(
challenge, expected_challenge,
"Challenge should match base64-url-encoded SHA256 of verifier"
);
assert!(
!challenge.contains('='),
"Challenge should not contain padding characters"
);
}
#[test]
fn test_generate_pkce_uniqueness() {
let (verifier1, challenge1) = generate_pkce();
let (verifier2, challenge2) = generate_pkce();
assert_ne!(
verifier1, verifier2,
"Multiple calls should generate unique verifiers"
);
assert_ne!(
challenge1, challenge2,
"Multiple calls should generate unique challenges"
);
}
}